Merge branch 'dev' into SES-1792]-Standardised-Message-Deletion-

pull/1518/head
ThomasSession 6 months ago
commit 38ac9dd97a

@ -13,8 +13,8 @@ configurations.forEach {
it.exclude module: "commons-logging"
}
def canonicalVersionCode = 382
def canonicalVersionName = "1.20.0"
def canonicalVersionCode = 383
def canonicalVersionName = "1.20.1"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,
@ -56,7 +56,7 @@ android {
splits {
abi {
enable true
enable !project.hasProperty('huawei') // huawei builds do not need the split variants
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
universalApk true
@ -118,6 +118,25 @@ android {
}
}
signingConfigs {
play {
if (project.hasProperty('SESSION_STORE_FILE')) {
storeFile file(SESSION_STORE_FILE)
storePassword SESSION_STORE_PASSWORD
keyAlias SESSION_KEY_ALIAS
keyPassword SESSION_KEY_PASSWORD
}
}
huawei {
if (project.hasProperty('SESSION_HUAWEI_STORE_FILE')) {
storeFile file(SESSION_HUAWEI_STORE_FILE)
storePassword SESSION_HUAWEI_STORE_PASSWORD
keyAlias SESSION_HUAWEI_KEY_ALIAS
keyPassword SESSION_HUAWEI_KEY_PASSWORD
}
}
}
flavorDimensions "distribution"
productFlavors {
play {
@ -129,6 +148,7 @@ android {
buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.ANDROID"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
buildConfigField 'String', 'PUSH_KEY_SUFFIX', '\"\"'
signingConfig signingConfigs.play
}
huawei {
@ -138,6 +158,7 @@ android {
buildConfigField "org.session.libsession.utilities.Device", "DEVICE", "org.session.libsession.utilities.Device.HUAWEI"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
buildConfigField 'String', 'PUSH_KEY_SUFFIX', '\"_HUAWEI\"'
signingConfig signingConfigs.huawei
}
website {
@ -150,13 +171,15 @@ android {
}
}
applicationVariants.forEach { variant ->
applicationVariants.configureEach { variant ->
variant.outputs.each { output ->
def abiName = output.getFilter("ABI") ?: 'universal'
def postFix = abiPostFix.get(abiName, 0)
def flavour = (variant.flavorName == 'huawei') ? "-huawei" : ""
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
output.outputFileName = output.outputFileName = "session-${variant.versionName}-${abiName}.apk"
output.outputFileName = output.outputFileName = "session-${variant.versionName}-${abiName}${flavour}.apk"
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
}
}

@ -62,7 +62,10 @@ class SessionDialogBuilder(val context: Context) {
// Main title entry point
fun title(text: String?) {
text(text, R.style.TextAppearance_Session_Dialog_Title) { setPadding(dp20, 0, dp20, 0) }
text(
text = text,
qaTag = context.getString(R.string.AccessibilityId_modalTitle),
style = R.style.TextAppearance_Session_Dialog_Title) { setPadding(dp20, 0, dp20, 0) }
}
// Convenience assessor for title that takes a string resource
@ -74,18 +77,24 @@ class SessionDialogBuilder(val context: Context) {
fun text(@StringRes id: Int, style: Int? = null) = text(context.getString(id), style)
fun text(text: CharSequence?, @StyleRes style: Int? = null) {
text(text, style) {
text(text = text, style = style) {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
.apply { updateMargins(dp40, 0, dp40, 0) }
}
}
private fun text(text: CharSequence?, @StyleRes style: Int? = null, modify: TextView.() -> Unit) {
private fun text(
text: CharSequence?,
qaTag: String = context.getString(R.string.AccessibilityId_modalMessage),
@StyleRes style: Int? = null,
modify: TextView.() -> Unit
) {
text ?: return
TextView(context, null, 0, style ?: R.style.TextAppearance_Session_Dialog_Message)
.apply {
setText(text)
textAlignment = View.TEXT_ALIGNMENT_CENTER
contentDescription = qaTag
modify()
}.let(topView::addView)
@ -166,7 +175,7 @@ class SessionDialogBuilder(val context: Context) {
textColor?.let{
setTextColor(it)
}
contentDescription = resources.getString(contentDescriptionRes)
contentDescription = resources.getString(text) // QA now wants the qa tag to mtch the button's text
layoutParams = LinearLayout.LayoutParams(WRAP_CONTENT, dp60, 1f)
setOnClickListener {
listener.invoke()

@ -57,6 +57,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
@ -732,9 +733,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
private fun restoreDraftIfNeeded() {
val mediaURI = intent.data
val mediaType = AttachmentManager.MediaType.from(intent.type)
val mimeType = MediaUtil.getMimeType(this, mediaURI)
if (mediaURI != null && mediaType != null) {
if (AttachmentManager.MediaType.IMAGE == mediaType || AttachmentManager.MediaType.GIF == mediaType || AttachmentManager.MediaType.VIDEO == mediaType) {
val media = Media(mediaURI, MediaUtil.getMimeType(this, mediaURI)!!, 0, 0, 0, 0, Optional.absent(), Optional.absent())
if (mimeType != null && (AttachmentManager.MediaType.IMAGE == mediaType || AttachmentManager.MediaType.GIF == mediaType || AttachmentManager.MediaType.VIDEO == mediaType)) {
val media = Media(mediaURI, mimeType, 0, 0, 0, 0, Optional.absent(), Optional.absent())
startActivityForResult(MediaSendActivity.buildEditorIntent(this, listOf( media ), viewModel.recipient!!, ""), PICK_FROM_LIBRARY)
return
} else {
@ -811,7 +813,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
if (shouldShowLegacy) {
val txt = Phrase.from(applicationContext, R.string.disappearingMessagesLegacy)
.put(NAME_KEY, legacyRecipient!!.name)
.put(NAME_KEY, legacyRecipient!!.toShortString())
.format()
binding?.outdatedBannerTextView?.text = txt
}
@ -1193,14 +1195,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
title(R.string.block)
text(
Phrase.from(context, R.string.blockDescription)
.put(NAME_KEY, recipient.name)
.put(NAME_KEY, recipient.toShortString())
.format()
)
dangerButton(R.string.block, R.string.AccessibilityId_blockConfirm) {
viewModel.block()
// Block confirmation toast added as per SS-64
val txt = Phrase.from(context, R.string.blockBlockedUser).put(NAME_KEY, recipient.name).format().toString()
val txt = Phrase.from(context, R.string.blockBlockedUser).put(NAME_KEY, recipient.toShortString()).format().toString()
Toast.makeText(context, txt, Toast.LENGTH_LONG).show()
if (deleteThread) {
@ -1251,7 +1253,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
title(R.string.blockUnblock)
text(
Phrase.from(context, R.string.blockUnblockName)
.put(NAME_KEY, recipient.name)
.put(NAME_KEY, recipient.toShortString())
.format()
)
dangerButton(R.string.blockUnblock, R.string.AccessibilityId_unblockConfirm) { viewModel.unblock() }
@ -1745,10 +1747,19 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
binding.inputBar.text = ""
binding.inputBar.cancelQuoteDraft()
binding.inputBar.cancelLinkPreviewDraft()
// Put the message in the database
message.id = smsDb.insertMessageOutbox(viewModel.threadId, outgoingTextMessage, false, message.sentTimestamp!!, null, true)
// Send it
MessageSender.send(message, recipient.address)
lifecycleScope.launch(Dispatchers.Default) {
// Put the message in the database
message.id = smsDb.insertMessageOutbox(
viewModel.threadId,
outgoingTextMessage,
false,
message.sentTimestamp!!,
null,
true
)
// Send it
MessageSender.send(message, recipient.address)
}
// Send a typing stopped message
ApplicationContext.getInstance(this).typingStatusSender.onTypingStopped(viewModel.threadId)
return Pair(recipient.address, sentTimestamp)
@ -2405,7 +2416,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
// Note: The adapter itemCount is zero based - so calling this with the itemCount in
// a non-zero based manner scrolls us to the bottom of the last message (including
// to the bottom of long messages as required by Jira SES-789 / GitHub 1364).
recyclerView.scrollToPosition(adapter.itemCount)
recyclerView.smoothScrollToPosition(adapter.itemCount)
}
}
}

@ -35,7 +35,7 @@ class DownloadDialog(private val recipient: Recipient) : DialogFragment() {
title(getString(R.string.attachmentsAutoDownloadModalTitle))
val explanation = Phrase.from(context, R.string.attachmentsAutoDownloadModalDescription)
.put(CONVERSATION_NAME_KEY, recipient.name)
.put(CONVERSATION_NAME_KEY, recipient.toShortString())
.format()
val spannable = SpannableStringBuilder(explanation)

@ -202,13 +202,17 @@ class MentionViewModel(
val sb = StringBuilder()
var offset = 0
for ((span, range) in spansWithRanges) {
// Add content before the mention span
sb.append(editable, offset, range.first)
// Add content before the mention span. There's a possibility of overlapping spans so we need to
// safe guard the start offset here to not go over our span's start.
val thisMentionStart = range.first
val lastMentionEnd = offset.coerceAtMost(thisMentionStart)
sb.append(editable, lastMentionEnd, thisMentionStart)
// Replace the mention span with "@public key"
sb.append('@').append(span.member.publicKey).append(' ')
offset = range.last + 1
// Safe guard offset to not go over the end of the editable.
offset = (range.last + 1).coerceAtMost(editable.length)
}
// Add the remaining content

@ -144,14 +144,17 @@ public class ThreadRecord extends DisplayRecord {
.format().toString();
} else if (MmsSmsColumns.Types.isMessageRequestResponse(type)) {
if (lastMessage.getRecipient().getAddress().serialize().equals(
TextSecurePreferences.getLocalNumber(context))) {
return UtilKt.getSubbedCharSequence(
context,
R.string.messageRequestYouHaveAccepted,
new Pair<>(NAME_KEY, getName())
);
try {
if (lastMessage.getRecipient().getAddress().serialize().equals(
TextSecurePreferences.getLocalNumber(context))) {
return UtilKt.getSubbedCharSequence(
context,
R.string.messageRequestYouHaveAccepted,
new Pair<>(NAME_KEY, getName())
);
}
}
catch (Exception e){} // the above can throw a null exception
return context.getString(R.string.messageRequestsAccepted);
} else if (getCount() == 0) {

@ -74,12 +74,10 @@ fun DebugMenu(
buttons = listOf(
DialogButtonModel(
text = GetString(R.string.cancel),
contentDescription = GetString(R.string.cancel),
onClick = { sendCommand(HideEnvironmentWarningDialog) }
),
DialogButtonModel(
text = GetString(R.string.ok),
contentDescription = GetString(R.string.ok),
onClick = { sendCommand(ChangeEnvironment) }
)
)

@ -71,6 +71,7 @@ import org.thoughtcrime.securesms.home.search.GlobalSearchViewModel
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import org.session.libsession.utilities.truncateIdForDisplay
import org.thoughtcrime.securesms.notifications.PushRegistry
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.preferences.SettingsActivity
@ -507,7 +508,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
showSessionDialog {
title(R.string.block)
text(Phrase.from(context, R.string.blockDescription)
.put(NAME_KEY, thread.recipient.name)
.put(NAME_KEY, thread.recipient.toShortString())
.format())
dangerButton(R.string.block, R.string.AccessibilityId_blockConfirm) {
lifecycleScope.launch(Dispatchers.IO) {
@ -518,7 +519,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
}
}
// Block confirmation toast added as per SS-64
val txt = Phrase.from(context, R.string.blockBlockedUser).put(NAME_KEY, thread.recipient.name).format().toString()
val txt = Phrase.from(context, R.string.blockBlockedUser).put(NAME_KEY, thread.recipient.toShortString()).format().toString()
Toast.makeText(context, txt, Toast.LENGTH_LONG).show()
}
cancelButton()
@ -528,7 +529,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
private fun unblockConversation(thread: ThreadRecord) {
showSessionDialog {
title(R.string.blockUnblock)
text(Phrase.from(context, R.string.blockUnblockName).put(NAME_KEY, thread.recipient.name).format())
text(Phrase.from(context, R.string.blockUnblockName).put(NAME_KEY, thread.recipient.toShortString()).format())
dangerButton(R.string.blockUnblock, R.string.AccessibilityId_unblockConfirm) {
lifecycleScope.launch(Dispatchers.IO) {
storage.setBlocked(listOf(thread.recipient), false)
@ -615,7 +616,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
if (recipient.name != null) {
title = getString(R.string.conversationsDelete)
message = Phrase.from(this.applicationContext, R.string.conversationsDeleteDescription)
.put(NAME_KEY, recipient.name)
.put(NAME_KEY, recipient.toShortString())
.format()
}
else {

@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.home.search
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.BufferOverflow
@ -13,11 +14,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import org.thoughtcrime.securesms.search.SearchRepository
import org.thoughtcrime.securesms.search.model.SearchResult
import javax.inject.Inject
@ -38,9 +41,14 @@ class GlobalSearchViewModel @Inject constructor(
.buffer(onBufferOverflow = BufferOverflow.DROP_OLDEST)
.mapLatest { query ->
if (query.trim().isEmpty()) {
// searching for 05 as contactDb#getAllContacts was not returning contacts
// without a nickname/name who haven't approved us.
GlobalSearchResult(query.toString(), searchRepository.queryContacts("05").first.toList())
withContext(Dispatchers.Default) {
// searching for 05 as contactDb#getAllContacts was not returning contacts
// without a nickname/name who haven't approved us.
GlobalSearchResult(
query.toString(),
searchRepository.queryContacts("05").first.toList()
)
}
} else {
// User input delay in case we get a new query within a few hundred ms this
// coroutine will be cancelled and the expensive query will not be run.

@ -231,8 +231,8 @@ private fun SaveAttachmentWarningDialog(
title = context.getString(R.string.warning),
text = context.resources.getString(R.string.attachmentsWarning),
buttons = listOf(
DialogButtonModel(GetString(R.string.save), GetString(R.string.AccessibilityId_saveAttachment), color = LocalColors.current.danger, onClick = onAccepted),
DialogButtonModel(GetString(android.R.string.cancel), GetString(R.string.AccessibilityId_cancel), dismissOnClick = true)
DialogButtonModel(GetString(R.string.save), color = LocalColors.current.danger, onClick = onAccepted),
DialogButtonModel(GetString(android.R.string.cancel), dismissOnClick = true)
)
)
}

@ -87,7 +87,7 @@ class MessageRequestsActivity : PassphraseRequiredActionBarActivity(), Conversat
showSessionDialog {
title(R.string.block)
text(Phrase.from(context, R.string.blockDescription)
.put(NAME_KEY, thread.recipient.name)
.put(NAME_KEY, thread.recipient.toShortString())
.format())
dangerButton(R.string.block, R.string.AccessibilityId_blockConfirm) {
doBlock()

@ -85,12 +85,10 @@ internal fun LandingScreen(
buttons = listOf(
DialogButtonModel(
text = GetString(R.string.onboardingTos),
contentDescription = GetString(R.string.AccessibilityId_onboardingTos),
onClick = openTerms
),
DialogButtonModel(
text = GetString(R.string.onboardingPrivacy),
contentDescription = GetString(R.string.AccessibilityId_onboardingPrivacy),
onClick = openPrivacyPolicy
)
)

@ -573,13 +573,11 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
buttons = listOf(
DialogButtonModel(
text = GetString(R.string.save),
contentDescription = GetString(R.string.AccessibilityId_save),
enabled = state is TempAvatar,
onClick = saveAvatar
),
DialogButtonModel(
text = GetString(R.string.remove),
contentDescription = GetString(R.string.AccessibilityId_remove),
color = LocalColors.current.danger,
enabled = state is UserAvatar || // can remove is the user has an avatar set
(state is TempAvatar && state.hasAvatar),

@ -26,7 +26,10 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import network.loki.messenger.R
import org.thoughtcrime.securesms.ui.AlertDialog
import org.thoughtcrime.securesms.ui.Cell
import org.thoughtcrime.securesms.ui.DialogButtonModel
import org.thoughtcrime.securesms.ui.GetString
import org.thoughtcrime.securesms.ui.SessionShieldIcon
import org.thoughtcrime.securesms.ui.components.QrImage
import org.thoughtcrime.securesms.ui.components.SlimOutlineButton
@ -45,8 +48,8 @@ import org.thoughtcrime.securesms.ui.theme.monospace
internal fun RecoveryPasswordScreen(
mnemonic: String,
seed: String? = null,
copyMnemonic:() -> Unit = {},
onHide:() -> Unit = {}
confirmHideRecovery: () -> Unit,
copyMnemonic:() -> Unit = {}
) {
Column(
verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing),
@ -57,7 +60,7 @@ internal fun RecoveryPasswordScreen(
.padding(horizontal = LocalDimensions.current.spacing)
) {
RecoveryPasswordCell(mnemonic, seed, copyMnemonic)
HideRecoveryPasswordCell(onHide)
HideRecoveryPasswordCell(confirmHideRecovery = confirmHideRecovery)
}
}
@ -151,7 +154,12 @@ private fun RecoveryPassword(mnemonic: String) {
}
@Composable
private fun HideRecoveryPasswordCell(onHide: () -> Unit = {}) {
private fun HideRecoveryPasswordCell(
confirmHideRecovery:() -> Unit
) {
var showHideRecoveryDialog by remember { mutableStateOf(false) }
var showHideRecoveryConfirmationDialog by remember { mutableStateOf(false) }
Cell {
Row(
modifier = Modifier.padding(LocalDimensions.current.smallSpacing)
@ -176,10 +184,44 @@ private fun HideRecoveryPasswordCell(onHide: () -> Unit = {}) {
.align(Alignment.CenterVertically)
.contentDescription(R.string.AccessibilityId_recoveryPasswordHideRecoveryPassword),
color = LocalColors.current.danger,
onClick = onHide
onClick = { showHideRecoveryDialog = true }
)
}
}
// recovery hide dialog
if(showHideRecoveryDialog) {
AlertDialog(
onDismissRequest = { showHideRecoveryDialog = false },
title = stringResource(R.string.recoveryPasswordHidePermanently),
text = stringResource(R.string.recoveryPasswordHidePermanentlyDescription1),
buttons = listOf(
DialogButtonModel(
GetString(R.string.theContinue),
color = LocalColors.current.danger,
onClick = { showHideRecoveryConfirmationDialog = true }
),
DialogButtonModel(GetString(android.R.string.cancel))
)
)
}
// recovery hide confirmation dialog
if(showHideRecoveryConfirmationDialog) {
AlertDialog(
onDismissRequest = { showHideRecoveryConfirmationDialog = false },
title = stringResource(R.string.recoveryPasswordHidePermanently),
text = stringResource(R.string.recoveryPasswordHidePermanentlyDescription2),
buttons = listOf(
DialogButtonModel(
GetString(R.string.yes),
color = LocalColors.current.danger,
onClick = confirmHideRecovery
),
DialogButtonModel(GetString(android.R.string.cancel))
)
)
}
}
@Preview
@ -188,6 +230,9 @@ private fun PreviewRecoveryPasswordScreen(
@PreviewParameter(SessionColorsParameterProvider::class) colors: ThemeColors
) {
PreviewTheme(colors) {
RecoveryPasswordScreen(mnemonic = "voyage urban toyed maverick peculiar tuxedo penguin tree grass building listen speak withdraw terminal plane")
RecoveryPasswordScreen(
mnemonic = "voyage urban toyed maverick peculiar tuxedo penguin tree grass building listen speak withdraw terminal plane",
confirmHideRecovery = {}
)
}
}

@ -24,33 +24,12 @@ class RecoveryPasswordActivity : BaseActionBarActivity() {
RecoveryPasswordScreen(
mnemonic = mnemonic,
seed = seed,
copyMnemonic = viewModel::copyMnemonic,
onHide = ::onHide
confirmHideRecovery = {
viewModel.permanentlyHidePassword()
finish()
},
copyMnemonic = viewModel::copyMnemonic
)
}
}
private fun onHide() {
showSessionDialog {
title(R.string.recoveryPasswordHidePermanently)
text(R.string.recoveryPasswordHidePermanentlyDescription1)
dangerButton(R.string.theContinue, R.string.AccessibilityId_theContinue) { onHideConfirm() }
cancelButton()
}
}
private fun onHideConfirm() {
showSessionDialog {
title(R.string.recoveryPasswordHidePermanently)
text(R.string.recoveryPasswordHidePermanentlyDescription2)
cancelButton()
dangerButton(
R.string.yes,
contentDescription = R.string.AccessibilityId_recoveryPasswordHidePermanentlyConfirm
) {
viewModel.permanentlyHidePassword()
finish()
}
}
}
}

@ -55,7 +55,6 @@ import org.thoughtcrime.securesms.ui.theme.bold
class DialogButtonModel(
val text: GetString,
val contentDescription: GetString = text,
val color: Color = Color.Unspecified,
val dismissOnClick: Boolean = true,
val enabled: Boolean = true,
@ -130,6 +129,7 @@ fun AlertDialog(
textAlign = TextAlign.Center,
style = LocalType.current.h7,
modifier = Modifier.padding(bottom = LocalDimensions.current.xxsSpacing)
.qaTag(stringResource(R.string.AccessibilityId_modalTitle))
)
}
text?.let {
@ -152,6 +152,7 @@ fun AlertDialog(
textAlign = TextAlign.Center,
style = textStyle,
modifier = textModifier
.qaTag(stringResource(R.string.AccessibilityId_modalMessage))
)
}
content()
@ -163,7 +164,7 @@ fun AlertDialog(
text = it.text(),
modifier = Modifier
.fillMaxHeight()
.contentDescription(it.contentDescription())
.qaTag(it.text.string())
.weight(1f),
color = it.color,
enabled = it.enabled
@ -201,13 +202,11 @@ fun OpenURLAlertDialog(
buttons = listOf(
DialogButtonModel(
text = GetString(R.string.open),
contentDescription = GetString(R.string.AccessibilityId_urlOpenBrowser),
color = LocalColors.current.danger,
onClick = { context.openUrl(url) }
),
DialogButtonModel(
text = GetString(android.R.string.copyUrl),
contentDescription = GetString(R.string.AccessibilityId_copy),
onClick = {
context.copyURLToClipboard(url)
Toast.makeText(context, R.string.copied, Toast.LENGTH_SHORT).show()
@ -297,7 +296,8 @@ fun LoadingDialog(
title?.let {
Text(
it,
modifier = Modifier.align(Alignment.CenterHorizontally),
modifier = Modifier.align(Alignment.CenterHorizontally)
.qaTag(stringResource(R.string.AccessibilityId_modalTitle)),
style = LocalType.current.large
)
}
@ -340,12 +340,10 @@ fun PreviewXCloseDialog() {
buttons = listOf(
DialogButtonModel(
text = GetString(R.string.onboardingTos),
contentDescription = GetString(R.string.AccessibilityId_onboardingTos),
onClick = {}
),
DialogButtonModel(
text = GetString(R.string.onboardingPrivacy),
contentDescription = GetString(R.string.AccessibilityId_onboardingPrivacy),
onClick = {}
)
),

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="50"
android:viewportHeight="50">
<path
android:pathData="M15.575,11.473L30.841,23.853L15.247,36.509C15.053,36.658 14.889,36.848 14.764,37.067C14.639,37.286 14.557,37.529 14.521,37.783C14.484,38.037 14.496,38.296 14.554,38.545C14.613,38.794 14.717,39.028 14.86,39.233C15.003,39.438 15.184,39.61 15.39,39.739C15.596,39.868 15.824,39.951 16.061,39.984C16.298,40.017 16.538,39.999 16.768,39.93C16.998,39.861 17.212,39.744 17.4,39.584L34.455,25.746C34.637,25.594 34.792,25.408 34.91,25.196C34.993,25.112 35.07,25.022 35.14,24.926C35.426,24.518 35.549,24.005 35.482,23.499C35.416,22.994 35.166,22.537 34.787,22.23L17.732,8.391C17.545,8.238 17.331,8.127 17.104,8.063C16.877,7.999 16.64,7.983 16.407,8.018C16.174,8.053 15.95,8.137 15.748,8.265C15.545,8.393 15.368,8.563 15.227,8.766C15.084,8.968 14.98,9.198 14.921,9.444C14.861,9.689 14.847,9.945 14.879,10.197C14.911,10.448 14.99,10.69 15.109,10.909C15.228,11.128 15.386,11.32 15.575,11.473Z"
android:fillColor="?android:textColorPrimary"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="50"
android:viewportHeight="50">
<path
android:pathData="M34.425,36.527L19.159,24.147L34.753,11.491C34.947,11.342 35.111,11.152 35.236,10.933C35.361,10.714 35.443,10.471 35.479,10.217C35.515,9.963 35.504,9.704 35.446,9.455C35.387,9.206 35.284,8.972 35.14,8.767C34.996,8.562 34.816,8.39 34.61,8.261C34.404,8.132 34.176,8.049 33.939,8.016C33.702,7.983 33.462,8.001 33.232,8.07C33.002,8.139 32.787,8.256 32.6,8.416L15.545,22.254C15.363,22.406 15.208,22.592 15.09,22.804C15.007,22.888 14.93,22.978 14.86,23.074C14.574,23.482 14.451,23.995 14.517,24.501C14.584,25.006 14.834,25.463 15.212,25.77L32.268,39.609C32.455,39.762 32.668,39.873 32.896,39.937C33.123,40.001 33.36,40.016 33.593,39.982C33.826,39.947 34.05,39.863 34.252,39.735C34.455,39.607 34.632,39.437 34.773,39.235C34.916,39.032 35.02,38.802 35.079,38.556C35.139,38.311 35.153,38.055 35.121,37.803C35.089,37.552 35.011,37.31 34.891,37.091C34.772,36.872 34.614,36.68 34.425,36.527Z"
android:fillColor="#000000"/>
</vector>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?searchBackgroundColor" />
<corners android:topLeftRadius="100dp" android:bottomLeftRadius="100dp" />
</shape>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?searchBackgroundColor" />
<corners android:topRightRadius="100dp" android:bottomRightRadius="100dp" />
</shape>

@ -179,6 +179,11 @@ class MentionViewModelTest {
// Should have normalised message with selected candidate
assertThat(mentionViewModel.normalizeMessageBody())
.isEqualTo("Hi @pubkey1 ")
// Should have correct normalised message even with the last space deleted
editable.delete(editable.length - 1, editable.length)
assertThat(mentionViewModel.normalizeMessageBody())
.isEqualTo("Hi @pubkey1 ")
}
}
}

@ -128,6 +128,9 @@
<string name="AccessibilityId_messageVoice">Voice message</string>
<string name="AccessibilityId_deliveryIndicator">Delivered</string>
<string name="AccessibilityId_message_request_config_message">Message request has been accepted</string>
<!-- Modals -->
<string name="AccessibilityId_modalTitle">Modal heading</string>
<string name="AccessibilityId_modalMessage">Modal description</string>
<!-- Delete message modal-->
<string name="AccessibilityId_deleteMessageCancel">Cancel deletion</string>
<string name="AccessibilityId_deleteMessageEveryone">Delete for everyone</string> <!-- ACL: Perhaps should be "AccessibilityId_clearMessagesForEveryone"? -->

@ -1 +0,0 @@
{"LanguageName": "Afrikaans", "LanguageCode": "af", "Week": "week", "Weeks": "weke", "Day": "dag", "Days": "dae", "Hour": "uur", "Hours": "ure", "Minute": "minuut", "Minutes": "minute", "Second": "tweede", "Seconds": "sekondes"}

@ -1 +0,0 @@
{"LanguageName": "Amharic", "LanguageCode": "am", "Week": "\u1233\u121d\u1295\u1275", "Weeks": "\u1233\u121d\u1295\u1273\u1275", "Day": "\u1240\u1295", "Days": "\u1240\u1293\u1275", "Hour": "\u1230\u12a0\u1275", "Hours": "\u1230\u12d3\u1273\u1275", "Minute": "\u12f0\u1242\u1243", "Minutes": "\u12f0\u1242\u1243\u12ce\u127d", "Second": "\u1201\u1208\u1270\u129b", "Seconds": "\u1230\u12a8\u1295\u12f6\u127d"}

@ -1 +0,0 @@
{"LanguageName": "Arabic", "LanguageCode": "ar", "Week": "\u0623\u0633\u0628\u0648\u0639", "Weeks": "\u0623\u0633\u0627\u0628\u064a\u0639", "Day": "\u064a\u0648\u0645", "Days": "\u0623\u064a\u0627\u0645", "Hour": "\u0633\u0627\u0639\u0629", "Hours": "\u0633\u0627\u0639\u0627\u062a", "Minute": "دقيقة", "Minutes": "دقائق", "Second": "\u062b\u0627\u0646\u064a\u0629", "Seconds": "\u062b\u0627\u0646\u064a\u0629"}

@ -1 +0,0 @@
{"LanguageName": "Azerbaijani", "LanguageCode": "az", "Week": "h\u0259ft\u0259", "Weeks": "h\u0259ft\u0259l\u0259r", "Day": "g\u00fcn", "Days": "g\u00fcnl\u0259r", "Hour": "saat", "Hours": "saat", "Minute": "d\u0259qiq\u0259", "Minutes": "d\u0259qiq\u0259", "Second": "ikinci", "Seconds": "saniy\u0259"}

@ -1 +0,0 @@
{"LanguageName": "Belarusian", "LanguageCode": "be", "Week": "\u0442\u044b\u0434\u0437\u0435\u043d\u044c", "Weeks": "\u0442\u044b\u0434\u043d\u044f\u045e", "Day": "\u0434\u0437\u0435\u043d\u044c", "Days": "\u0434\u0437\u0451\u043d", "Hour": "\u0433\u0430\u0434\u0437\u0456\u043d\u0443", "Hours": "\u0433\u0430\u0434\u0437\u0456\u043d\u044b", "Minute": "\u0445\u0432\u0456\u043b\u0456\u043d\u0430", "Minutes": "\u0445\u0432\u0456\u043b\u0456\u043d", "Second": "\u0434\u0440\u0443\u0433\u0456", "Seconds": "\u0441\u0435\u043a\u0443\u043d\u0434"}

@ -1 +0,0 @@
{"LanguageName": "Bulgarian", "LanguageCode": "bg", "Week": "\u0441\u0435\u0434\u043c\u0438\u0446\u0430", "Weeks": "\u0441\u0435\u0434\u043c\u0438\u0446\u0438", "Day": "\u0434\u0435\u043d", "Days": "\u0434\u043d\u0438", "Hour": "\u0447\u0430\u0441", "Hours": "\u0447\u0430\u0441\u0430", "Minute": "\u043c\u0438\u043d\u0443\u0442\u0430", "Minutes": "\u043c\u0438\u043d\u0443\u0442\u0438", "Second": "\u0432\u0442\u043e\u0440\u043e", "Seconds": "\u0441\u0435\u043a\u0443\u043d\u0434\u0438"}

@ -1 +0,0 @@
{"LanguageName": "Bengali", "LanguageCode": "bn", "Week": "\u09b8\u09aa\u09cd\u09a4\u09be\u09b9", "Weeks": "\u09b8\u09aa\u09cd\u09a4\u09be\u09b9", "Day": "\u09a6\u09bf\u09a8", "Days": "\u09a6\u09bf\u09a8", "Hour": "\u0998\u09a8\u09cd\u099f\u09be", "Hours": "\u0998\u09a8\u09cd\u099f\u09be\u09b0", "Minute": "\u09ae\u09bf\u09a8\u09bf\u099f", "Minutes": "\u09ae\u09bf\u09a8\u09bf\u099f", "Second": "\u09a6\u09cd\u09ac\u09bf\u09a4\u09c0\u09af\u09bc", "Seconds": "\u09b8\u09c7\u0995\u09c7\u09a8\u09cd\u09a1"}

@ -1 +0,0 @@
{"LanguageName": "Bosnian", "LanguageCode": "bs", "Week": "sedmica", "Weeks": "weken", "Day": "dag", "Days": "dagen", "Hour": "uur", "Hours": "uur", "Minute": "minuut", "Minutes": "minuten", "Second": "seconde", "Seconds": "seconden"}

@ -1 +0,0 @@
{"LanguageName": "Catalan", "LanguageCode": "ca", "Week": "setmana", "Weeks": "setmanes", "Day": "dia", "Days": "dies", "Hour": "hores", "Hours": "hores", "Minute": "minut", "Minutes": "minuts", "Second": "segon", "Seconds": "segons"}

@ -1 +0,0 @@
{"LanguageName": "Corsican", "LanguageCode": "co", "Week": "settimana", "Weeks": "settimane", "Day": "ghjornu", "Days": "ghjorni", "Hour": "ora", "Hours": "ore", "Minute": "minutu", "Minutes": "minuti", "Second": "sicondu", "Seconds": "seconde"}

@ -1 +0,0 @@
{"LanguageName": "Czech", "LanguageCode": "cs", "Week": "t\u00fdden", "Weeks": "t\u00fddn\u016f", "Day": "den", "Days": "dn\u00ed", "Hour": "hodina", "Hours": "hodin", "Minute": "minuta", "Minutes": "minut", "Second": "druh\u00fd", "Seconds": "sekundy"}

@ -1 +0,0 @@
{"LanguageName": "Welsh", "LanguageCode": "cy", "Week": "wythnos", "Weeks": "wythnosau", "Day": "Dydd", "Days": "dyddiau", "Hour": "awr", "Hours": "oriau", "Minute": "munud", "Minutes": "munudau", "Second": "ail", "Seconds": "eiliadau"}

@ -1 +0,0 @@
{"LanguageName": "Danish", "LanguageCode": "da", "Week": "uge", "Weeks": "uger", "Day": "dag", "Days": "dage", "Hour": "time", "Hours": "timer", "Minute": "minut", "Minutes": "minutter", "Second": "anden", "Seconds": "sekunder"}

@ -1 +0,0 @@
{"LanguageName": "German", "LanguageCode": "de", "Week": "Woche", "Weeks": "Wochen", "Day": "Tag", "Days": "Tage", "Hour": "Stunde", "Hours": "Std.", "Minute": "Minute", "Minutes": "Protokoll", "Second": "zweite", "Seconds": "Sekunden"}

@ -1 +0,0 @@
{"LanguageName": "Greek", "LanguageCode": "el", "Week": "\u03b5\u03b2\u03b4\u03bf\u03bc\u03ac\u03b4\u03b1", "Weeks": "\u03b5\u03b2\u03b4\u03bf\u03bc\u03ac\u03b4\u03b5\u03c2", "Day": "\u03b7\u03bc\u03ad\u03c1\u03b1", "Days": "\u03b7\u03bc\u03ad\u03c1\u03b5\u03c2", "Hour": "\u03ce\u03c1\u03b1", "Hours": "\u03ce\u03c1\u03b5\u03c2", "Minute": "\u03bb\u03b5\u03c0\u03c4\u03cc", "Minutes": "\u03bb\u03b5\u03c0\u03c4\u03ac", "Second": "\u03b4\u03b5\u03cd\u03c4\u03b5\u03c1\u03bf\u03c2", "Seconds": "\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1"}

@ -1 +0,0 @@
{"LanguageName": "English", "LanguageCode": "en", "Week": "week", "Weeks": "weeks", "Day": "day", "Days": "days", "Hour": "hour", "Hours": "hours", "Minute": "minute", "Minutes": "minutes", "Second": "second", "Seconds": "seconds"}

@ -1 +0,0 @@
{"LanguageName": "Esperanto", "LanguageCode": "eo", "Week": "semajno", "Weeks": "semajnoj", "Day": "tago", "Days": "tagoj", "Hour": "horo", "Hours": "horoj", "Minute": "minuto", "Minutes": "minutoj", "Second": "dua", "Seconds": "sekundoj"}

@ -1 +0,0 @@
{"LanguageName": "Spanish", "LanguageCode": "es", "Week": "semana", "Weeks": "semanas", "Day": "d\u00eda", "Days": "d\u00edas", "Hour": "hora", "Hours": "horas", "Minute": "minuto", "Minutes": "minutos", "Second": "segundo", "Seconds": "segundos"}

@ -1 +0,0 @@
{"LanguageName": "Estonian", "LanguageCode": "et", "Week": "n\u00e4dal", "Weeks": "n\u00e4dalaid", "Day": "p\u00e4eval", "Days": "p\u00e4evadel", "Hour": "tund", "Hours": "tundi", "Minute": "minut", "Minutes": "minutit", "Second": "teiseks", "Seconds": "sekundit"}

@ -1 +0,0 @@
{"LanguageName": "Basque", "LanguageCode": "eu", "Week": "astea", "Weeks": "asteak", "Day": "eguna", "Days": "egunak", "Hour": "ordua", "Hours": "orduak", "Minute": "minutua", "Minutes": "minutu", "Second": "bigarrena", "Seconds": "segundoak"}

@ -1 +0,0 @@
{"LanguageName": "Persian", "LanguageCode": "fa", "Week": "\u0647\u0641\u062a\u0647", "Weeks": "\u0647\u0641\u062a\u0647 \u0647\u0627", "Day": "\u0631\u0648\u0632", "Days": "\u0631\u0648\u0632\u0647\u0627", "Hour": "\u0633\u0627\u0639\u062a", "Hours": "\u0633\u0627\u0639\u062a \u0647\u0627", "Minute": "\u062f\u0642\u06cc\u0642\u0647", "Minutes": "\u062f\u0642\u0627\u06cc\u0642", "Second": "\u062f\u0648\u0645\u06cc\u0646", "Seconds": "\u062b\u0627\u0646\u06cc\u0647"}

@ -1 +0,0 @@
{"LanguageName": "Finnish", "LanguageCode": "fi", "Week": "viikko", "Weeks": "viikkoa", "Day": "p\u00e4iv\u00e4", "Days": "p\u00e4iv\u00e4\u00e4", "Hour": "tunnin", "Hours": "tuntia", "Minute": "minuutti", "Minutes": "p\u00f6yt\u00e4kirja", "Second": "toinen", "Seconds": "sekuntia"}

@ -1 +0,0 @@
{"LanguageName": "Filipino", "LanguageCode": "fil", "Week": "linggo", "Weeks": "linggo", "Day": "araw", "Days": "araw", "Hour": "oras", "Hours": "oras", "Minute": "minuto", "Minutes": "minuto", "Second": "pangalawa", "Seconds": "segundo"}

@ -1 +0,0 @@
{"LanguageName": "French", "LanguageCode": "fr", "Week": "semaine", "Weeks": "semaines", "Day": "jour", "Days": "jours", "Hour": "heure", "Hours": "heures", "Minute": "minute", "Minutes": "minutes", "Second": "seconde", "Seconds": "secondes"}

@ -1 +0,0 @@
{"LanguageName": "Frisian", "LanguageCode": "fy", "Week": "wike", "Weeks": "wiken", "Day": "dei", "Days": "dagen", "Hour": "oere", "Hours": "oeren", "Minute": "min\u00fat", "Minutes": "minuten", "Second": "twadde", "Seconds": "sekonden"}

@ -1 +0,0 @@
{"LanguageName": "Scots Gaelic", "LanguageCode": "gd", "Week": "seachdain", "Weeks": "seachdainean", "Day": "latha", "Days": "l\u00e0ithean", "Hour": "uair", "Hours": "uairean", "Minute": "mionaid", "Minutes": "mionaidean", "Second": "an d\u00e0rna", "Seconds": "diog"}

@ -1 +0,0 @@
{"LanguageName": "Galician", "LanguageCode": "gl", "Week": "semana", "Weeks": "semanas", "Day": "d\u00eda", "Days": "d\u00edas", "Hour": "hora", "Hours": "horas", "Minute": "minuto", "Minutes": "minutos", "Second": "segundo", "Seconds": "segundos"}

@ -1 +0,0 @@
{"LanguageName": "Gujarati", "LanguageCode": "gu", "Week": "\u0ab8\u0aaa\u0acd\u0aa4\u0abe\u0ab9", "Weeks": "\u0a85\u0aa0\u0ab5\u0abe\u0aa1\u0abf\u0aaf\u0abe", "Day": "\u0aa6\u0abf\u0ab5\u0ab8", "Days": "\u0aa6\u0abf\u0ab5\u0ab8", "Hour": "\u0a95\u0ab2\u0abe\u0a95", "Hours": "\u0a95\u0ab2\u0abe\u0a95", "Minute": "\u0aae\u0abf\u0aa8\u0abf\u0a9f", "Minutes": "\u0aae\u0abf\u0aa8\u0abf\u0a9f", "Second": "\u0aac\u0ac0\u0a9c\u0ac1\u0a82", "Seconds": "\u0ab8\u0ac7\u0a95\u0aa8\u0acd\u0aa1"}

@ -1 +0,0 @@
{"LanguageName": "Hausa", "LanguageCode": "ha", "Week": "mako", "Weeks": "makonni", "Day": "rana", "Days": "kwanaki", "Hour": "awa", "Hours": "hours", "Minute": "minti", "Minutes": "mintuna", "Second": "na biyu", "Seconds": "seconds"}

@ -1 +0,0 @@
{"LanguageName": "Hawaiian", "LanguageCode": "haw", "Week": "pule", "Weeks": "pule pule", "Day": "l\u0101", "Days": "l\u0101", "Hour": "hola", "Hours": "hola", "Minute": "minuke", "Minutes": "minuke", "Second": "ka lua", "Seconds": "kekona"}

@ -1 +0,0 @@
{"LanguageName": "Hindi", "LanguageCode": "hi", "Week": "\u0938\u092a\u094d\u0924\u093e\u0939", "Weeks": "\u0939\u092b\u094d\u0924\u094b\u0902", "Day": "\u0926\u093f\u0928", "Days": "\u0926\u093f\u0928", "Hour": "\u0918\u0902\u091f\u093e", "Hours": "\u0918\u0902\u091f\u0947", "Minute": "\u092e\u093f\u0928\u091f", "Minutes": "\u092e\u093f\u0928\u091f", "Second": "\u0926\u0942\u0938\u0930\u093e", "Seconds": "\u0938\u0947\u0915\u0902\u0921"}

@ -1 +0,0 @@
{"LanguageName": "Hmong", "LanguageCode": "hmn", "Week": "lub lim tiam", "Weeks": "lub lis piam", "Day": "hnub", "Days": "hnub", "Hour": "teev", "Hours": "teev", "Minute": "feeb", "Minutes": "feeb", "Second": "thib ob", "Seconds": "vib nas this"}

@ -1 +0,0 @@
{"LanguageName": "Croatian", "LanguageCode": "hr", "Week": "tjedan", "Weeks": "tjedni", "Day": "dan", "Days": "dana", "Hour": "sat", "Hours": "sati", "Minute": "minuta", "Minutes": "minuta", "Second": "drugi", "Seconds": "sekundi"}

@ -1 +0,0 @@
{"LanguageName": "Haitian Creole", "LanguageCode": "ht", "Week": "sem\u00e8n", "Weeks": "sem\u00e8n", "Day": "jou", "Days": "jou", "Hour": "\u00e8dtan", "Hours": "\u00e8dtan", "Minute": "minit", "Minutes": "minit", "Second": "dezy\u00e8m", "Seconds": "segonn"}

@ -1 +0,0 @@
{"LanguageName": "Hungarian", "LanguageCode": "hu", "Week": "h\u00e9t", "Weeks": "h\u00e9tig", "Day": "nap", "Days": "napok", "Hour": "\u00f3ra", "Hours": "\u00f3r\u00e1k", "Minute": "perc", "Minutes": "percek", "Second": "m\u00e1sodik", "Seconds": "m\u00e1sodpercig"}

@ -1 +0,0 @@
{"LanguageName": "Aermenian", "LanguageCode": "hy", "Week": "\u0576\u0565\u0564\u0561\u056c", "Weeks": "n\u00e4dalaid", "Day": "p\u00e4eval", "Days": "p\u00e4evadel", "Hour": "\u057f\u0578\u0582\u0576", "Hours": "\u057f\u0578\u0582\u0576\u0564\u056b", "Minute": "\u0580\u0578\u057a\u0565", "Minutes": "\u0580\u0578\u057a\u0565", "Second": "teiseks", "Seconds": "\u057d\u0565\u056f\u0578\u0582\u0576\u0564\u056b\u057f"}

@ -1 +0,0 @@
{"LanguageName": "Indonesian", "LanguageCode": "id", "Week": "pekan", "Weeks": "minggu", "Day": "hari", "Days": "hari", "Hour": "jam", "Hours": "jam", "Minute": "menit", "Minutes": "menit", "Second": "Kedua", "Seconds": "detik"}

@ -1 +0,0 @@
{"LanguageName": "Igbo", "LanguageCode": "ig", "Week": "izu", "Weeks": "izu", "Day": "\u1ee5b\u1ecdch\u1ecb", "Days": "\u1ee5b\u1ecdch\u1ecb", "Hour": "awa", "Hours": "awa", "Minute": "nkeji", "Minutes": "nkeji", "Second": "nke ab\u1ee5\u1ecd", "Seconds": "sek\u1ecdnd"}

@ -1 +0,0 @@
{"LanguageName": "Icelandic", "LanguageCode": "is", "Week": "vika", "Weeks": "vikur", "Day": "dagur", "Days": "daga", "Hour": "klukkustund", "Hours": "klukkustundir", "Minute": "m\u00edn\u00fatu", "Minutes": "m\u00edn\u00fatur", "Second": "anna\u00f0", "Seconds": "sek\u00fandur"}

@ -1 +0,0 @@
{"LanguageName": "Italian", "LanguageCode": "it", "Week": "settimana", "Weeks": "settimane", "Day": "giorno", "Days": "giorni", "Hour": "ora", "Hours": "ore", "Minute": "minuto", "Minutes": "minuti", "Second": "secondo", "Seconds": "secondi"}

@ -1 +0,0 @@
{"LanguageName": "Hebrew", "LanguageCode": "iw", "Week": "\u05e9\u05c1\u05b8\u05d1\u05d5\u05bc\u05e2\u05b7", "Weeks": "\u05e9\u05d1\u05d5\u05e2\u05d5\u05ea", "Day": "\u05d9\u05b0\u05d5\u05b9\u05dd", "Days": "\u05d9\u05de\u05d9\u05dd", "Hour": "\u05e9\u05c1\u05b8\u05e2\u05b8\u05d4", "Hours": "\u05e9\u05e2\u05d4 (\u05d5\u05ea", "Minute": "\u05d3\u05b7\u05e7\u05b8\u05d4", "Minutes": "\u05d3\u05e7\u05d5\u05ea", "Second": "\u05e9\u05c1\u05b0\u05e0\u05b4\u05d9\u05b8\u05d4", "Seconds": "\u05e9\u05e0\u05d9\u05d5\u05ea"}

@ -1 +0,0 @@
{"LanguageName": "Japanese", "LanguageCode": "ja", "Week": "週間", "Weeks": "週間", "Day": "日", "Days": "日間", "Hour": "時間", "Hours": "時間", "Minute": "分", "Minutes": "分", "Second": "秒", "Seconds": "秒"}

@ -1 +0,0 @@
{"LanguageName": "Javanese", "LanguageCode": "jv", "Week": "minggu", "Weeks": "minggu", "Day": "dina", "Days": "dina", "Hour": "jam", "Hours": "jam", "Minute": "menit", "Minutes": "menit", "Second": "kapindho", "Seconds": "detik"}

@ -1 +0,0 @@
{"LanguageName": "Georgian", "LanguageCode": "ka", "Week": "\u10d9\u10d5\u10d8\u10e0\u10d0", "Weeks": "\u10d9\u10d5\u10d8\u10e0\u10d4\u10d1\u10d8", "Day": "\u10d3\u10e6\u10d4\u10e1", "Days": "\u10d3\u10e6\u10d4\u10d4\u10d1\u10d8", "Hour": "\u10e1\u10d0\u10d0\u10d7\u10d8", "Hours": "\u10e1\u10d0\u10d0\u10d7\u10d4\u10d1\u10d8", "Minute": "\u10ec\u10e3\u10d7\u10d8", "Minutes": "\u10ec\u10e3\u10d7\u10d4\u10d1\u10d8", "Second": "\u10db\u10d4\u10dd\u10e0\u10d4", "Seconds": "\u10ec\u10d0\u10db\u10d8"}

@ -1 +0,0 @@
{"LanguageName": "Kazakh", "LanguageCode": "kk", "Week": "\u0430\u043f\u0442\u0430", "Weeks": "\u0430\u043f\u0442\u0430", "Day": "\u043a\u04af\u043d\u0456", "Days": "\u043a\u04af\u043d\u0434\u0435\u0440", "Hour": "\u0441\u0430\u0493\u0430\u0442", "Hours": "\u0441\u0430\u0493\u0430\u0442", "Minute": "\u043c\u0438\u043d\u0443\u0442", "Minutes": "\u043c\u0438\u043d\u0443\u0442", "Second": "\u0435\u043a\u0456\u043d\u0448\u0456", "Seconds": "\u0441\u0435\u043a\u0443\u043d\u0434"}

@ -1 +0,0 @@
{"LanguageName": "Khmer", "LanguageCode": "km", "Week": "\u179f\u1794\u17d2\u178f\u17b6\u17a0\u17cd", "Weeks": "\u179f\u1794\u17d2\u178f\u17b6\u17a0\u17cd", "Day": "\u1790\u17d2\u1784\u17c3", "Days": "\u1790\u17d2\u1784\u17c3", "Hour": "\u1798\u17c9\u17c4\u1784", "Hours": "\u1798\u17c9\u17c4\u1784", "Minute": "\u1793\u17b6\u1791\u17b8", "Minutes": "\u1793\u17b6\u1791\u17b8", "Second": "\u1791\u17b8\u1796\u17b8\u179a", "Seconds": "\u179c\u17b7\u1793\u17b6\u1791\u17b8"}

@ -1 +0,0 @@
{"LanguageName": "Kannada", "LanguageCode": "kn", "Week": "\u0cb5\u0cbe\u0cb0", "Weeks": "\u0cb5\u0cbe\u0cb0\u0c97\u0cb3\u0cc1", "Day": "\u0ca6\u0cbf\u0ca8", "Days": "\u0ca6\u0cbf\u0ca8\u0c97\u0cb3\u0cc1", "Hour": "\u0c97\u0c82\u0c9f\u0cc6", "Hours": "\u0c97\u0c82\u0c9f\u0cc6\u0c97\u0cb3\u0cc1", "Minute": "\u0ca8\u0cbf\u0cae\u0cbf\u0cb7", "Minutes": "\u0ca8\u0cbf\u0cae\u0cbf\u0cb7\u0c97\u0cb3\u0cc1", "Second": "\u0c8e\u0cb0\u0ca1\u0ca8\u0cc6\u0cd5", "Seconds": "\u0cb8\u0cc6\u0c95\u0cc6\u0c82\u0ca1\u0cc1\u0c97\u0cb3\u0cc1"}

@ -1 +0,0 @@
{"LanguageName": "Korean", "LanguageCode": "ko", "Week": "\uc8fc", "Weeks": "\uc8fc", "Day": "\ub0ae", "Days": "\ub0a0", "Hour": "\uc2dc\uac04", "Hours": "\uc2dc\uac04", "Minute": "\ubd84", "Minutes": "\ubd84", "Second": "\ub450\ubc88\uc9f8", "Seconds": "\ucd08"}

@ -1 +0,0 @@
{"LanguageName": "Kurdish", "LanguageCode": "ku", "Week": "hefte", "Weeks": "hefteyan", "Day": "roj", "Days": "rojan", "Hour": "seet", "Hours": "saetan", "Minute": "deqqe", "Minutes": "minutes", "Second": "duyem", "Seconds": "seconds"}

@ -1 +0,0 @@
{"LanguageName": "Kyrgyz", "LanguageCode": "ky", "Week": "\u0436\u0443\u043c\u0430", "Weeks": "\u0436\u0443\u043c\u0430", "Day": "\u043a\u04af\u043d", "Days": "\u043a\u04af\u043d", "Hour": "\u0441\u0430\u0430\u0442", "Hours": "\u0441\u0430\u0430\u0442", "Minute": "\u043c\u04af\u043d\u04e9\u0442", "Minutes": "\u043c\u04af\u043d\u04e9\u0442", "Second": "\u044d\u043a\u0438\u043d\u0447\u0438", "Seconds": "\u0441\u0435\u043a\u0443\u043d\u0434"}

@ -1 +0,0 @@
{"LanguageName": "Luxembourgish", "LanguageCode": "lb", "Week": "Woch", "Weeks": "Wochen", "Day": "Dag", "Days": "Deeg", "Hour": "Stonn", "Hours": "Stonnen", "Minute": "Minutt", "Minutes": "Minutten", "Second": "zweeten", "Seconds": "Sekonnen"}

@ -1 +0,0 @@
{"LanguageName": "Lao", "LanguageCode": "lo", "Week": "\u0ead\u0eb2\u0e97\u0eb4\u0e94", "Weeks": "\u0ead\u0eb2\u0e97\u0eb4\u0e94", "Day": "\u0ea1\u0eb7\u0ec9", "Days": "\u0ea1\u0eb7\u0ec9", "Hour": "\u0e8a\u0ebb\u0ec8\u0ea7\u0ec2\u0ea1\u0e87", "Hours": "\u0e8a\u0ebb\u0ec8\u0ea7\u0ec2\u0ea1\u0e87", "Minute": "\u0e99\u0eb2\u0e97\u0eb5", "Minutes": "\u0e99\u0eb2\u0e97\u0eb5", "Second": "\u0e97\u0eb5\u0eaa\u0ead\u0e87", "Seconds": "\u0ea7\u0eb4\u0e99\u0eb2\u0e97\u0eb5"}

@ -1 +0,0 @@
{"LanguageName": "Lithuanian", "LanguageCode": "lt", "Week": "savait\u0119", "Weeks": "savaites", "Day": "dien\u0105", "Days": "dien\u0173", "Hour": "valand\u0105", "Hours": "valand\u0173", "Minute": "minut\u0117", "Minutes": "minu\u010di\u0173", "Second": "antra", "Seconds": "sekund\u017ei\u0173"}

@ -1 +0,0 @@
{"LanguageName": "Latvian", "LanguageCode": "lv", "Week": "ned\u0113\u013ca", "Weeks": "ned\u0113\u013cas", "Day": "diena", "Days": "dienas", "Hour": "stunda", "Hours": "stundas", "Minute": "min\u016bte", "Minutes": "min\u016btes", "Second": "otrais", "Seconds": "sekundes"}

@ -1 +0,0 @@
{"LanguageName": "Malagasy", "LanguageCode": "mg", "Week": "herinandro", "Weeks": "HERINANDRON'NY", "Day": "andro", "Days": "andro", "Hour": "ora", "Hours": "ORA", "Minute": "minitra", "Minutes": "minitra", "Second": "FAHAROA", "Seconds": "segondra"}

@ -1 +0,0 @@
{"LanguageName": "Maori", "LanguageCode": "mi", "Week": "wiki", "Weeks": "wiki", "Day": "r\u0101", "Days": "nga ra", "Hour": "haora", "Hours": "haora", "Minute": "meneti", "Minutes": "meneti", "Second": "tuarua", "Seconds": "h\u0113kona"}

@ -1 +0,0 @@
{"LanguageName": "Macedonian", "LanguageCode": "mk", "Week": "\u043d\u0435\u0434\u0435\u043b\u0430", "Weeks": "\u043d\u0435\u0434\u0435\u043b\u0438", "Day": "\u0434\u0435\u043d", "Days": "\u0434\u0435\u043d\u043e\u0432\u0438", "Hour": "\u0447\u0430\u0441", "Hours": "\u0447\u0430\u0441\u043e\u0432\u0438", "Minute": "\u043c\u0438\u043d\u0443\u0442\u0430", "Minutes": "\u043c\u0438\u043d\u0443\u0442\u0438", "Second": "\u0432\u0442\u043e\u0440\u043e", "Seconds": "\u0441\u0435\u043a\u0443\u043d\u0434\u0438"}

@ -1 +0,0 @@
{"LanguageName": "Malayalam", "LanguageCode": "ml", "Week": "\u0d06\u0d34\u0d4d\u0d1a", "Weeks": "\u0d06\u0d34\u0d4d\u0d1a\u0d15\u0d7e", "Day": "\u0d26\u0d3f\u0d35\u0d38\u0d02", "Days": "\u0d26\u0d3f\u0d35\u0d38\u0d19\u0d4d\u0d19\u0d33\u0d3f\u0d7d", "Hour": "\u0d2e\u0d23\u0d3f\u0d15\u0d4d\u0d15\u0d42\u0d7c", "Hours": "\u0d2e\u0d23\u0d3f\u0d15\u0d4d\u0d15\u0d42\u0d31\u0d41\u0d15\u0d7e", "Minute": "\u0d2e\u0d3f\u0d28\u0d3f\u0d31\u0d4d\u0d31\u0d4d", "Minutes": "\u0d2e\u0d3f\u0d28\u0d3f\u0d31\u0d4d\u0d31\u0d4d", "Second": "\u0d30\u0d23\u0d4d\u0d1f\u0d3e\u0d2e\u0d24\u0d4d\u0d24\u0d47\u0d24\u0d4d", "Seconds": "\u0d38\u0d46\u0d15\u0d4d\u0d15\u0d7b\u0d4d\u0d31\u0d41\u0d15\u0d7e"}

@ -1 +0,0 @@
{"LanguageName": "Mongolian", "LanguageCode": "mn", "Week": "\u0434\u043e\u043b\u043e\u043e \u0445\u043e\u043d\u043e\u0433", "Weeks": "\u0434\u043e\u043b\u043e\u043e \u0445\u043e\u043d\u043e\u0433", "Day": "\u04e9\u0434\u04e9\u0440", "Days": "\u04e9\u0434\u0440\u04af\u04af\u0434", "Hour": "\u0446\u0430\u0433", "Hours": "\u0446\u0430\u0433", "Minute": "\u043c\u0438\u043d\u0443\u0442", "Minutes": "\u043c\u0438\u043d\u0443\u0442", "Second": "\u0445\u043e\u0451\u0440\u0434\u0443\u0433\u0430\u0430\u0440\u0442", "Seconds": "\u0441\u0435\u043a\u0443\u043d\u0434"}

@ -1 +0,0 @@
{"LanguageName": "Marathi", "LanguageCode": "mr", "Week": "\u0906\u0920\u0935\u0921\u093e", "Weeks": "\u0906\u0920\u0935\u0921\u0947", "Day": "\u0926\u093f\u0935\u0938", "Days": "\u0926\u093f\u0935\u0938", "Hour": "\u0924\u093e\u0938", "Hours": "\u0924\u093e\u0938", "Minute": "\u092e\u093f\u0928\u093f\u091f", "Minutes": "\u092e\u093f\u0928\u093f\u091f\u0947", "Second": "\u0926\u0941\u0938\u0930\u093e", "Seconds": "\u0938\u0947\u0915\u0902\u0926"}

@ -1 +0,0 @@
{"LanguageName": "Malay", "LanguageCode": "ms", "Week": "minggu", "Weeks": "minggu", "Day": "hari", "Days": "hari", "Hour": "jam", "Hours": "Jam", "Minute": "minit", "Minutes": "minit", "Second": "kedua", "Seconds": "detik"}

@ -1 +0,0 @@
{"LanguageName": "Maltese", "LanguageCode": "mt", "Week": "\u0121img\u0127a", "Weeks": "\u0121img\u0127at", "Day": "jum", "Days": "jiem", "Hour": "sieg\u0127a", "Hours": "sig\u0127at", "Minute": "minuta", "Minutes": "minuti", "Second": "it-tieni", "Seconds": "sekondi"}

@ -1 +0,0 @@
{"LanguageName": "Myanmar (Burmese)", "LanguageCode": "my", "Week": "\u1010\u1005\u103a\u1015\u1010\u103a", "Weeks": "\u101b\u1000\u103a\u101e\u1010\u1039\u1010\u1015\u1010\u103a", "Day": "\u1014\u1031\u1037", "Days": "\u1014\u1031\u1037\u101b\u1000\u103a\u1019\u103b\u102c\u1038", "Hour": "\u1014\u102c\u101b\u102e", "Hours": "\u1014\u102c\u101b\u102e", "Minute": "\u1019\u102d\u1014\u1005\u103a", "Minutes": "\u1019\u102d\u1014\u1005\u103a\u1019\u103b\u102c\u1038", "Second": "\u1012\u102f\u1010\u102d\u101a", "Seconds": "\u1005\u1000\u1039\u1000\u1014\u1037\u103a"}

@ -1 +0,0 @@
{"LanguageName": "Nepali", "LanguageCode": "ne", "Week": "\u0939\u092a\u094d\u0924\u093e", "Weeks": "\u0939\u092a\u094d\u0924\u093e\u0939\u0930\u0942", "Day": "\u0926\u093f\u0928", "Days": "\u0926\u093f\u0928\u0939\u0930\u0942", "Hour": "\u0918\u0923\u094d\u091f\u093e", "Hours": "\u0918\u0923\u094d\u091f\u093e", "Minute": "\u092e\u093f\u0928\u0947\u091f", "Minutes": "\u092e\u093f\u0928\u0947\u091f", "Second": "\u0926\u094b\u0938\u094d\u0930\u094b", "Seconds": "\u0938\u0947\u0915\u0947\u0928\u094d\u0921"}

@ -1 +0,0 @@
{"LanguageName": "Dutch", "LanguageCode": "nl", "Week": "week", "Weeks": "weken", "Day": "dag", "Days": "dagen", "Hour": "uur", "Hours": "uur", "Minute": "minuut", "Minutes": "minuten", "Second": "seconde", "Seconds": "seconden"}

@ -1 +0,0 @@
{"LanguageName": "Norwegian", "LanguageCode": "no", "Week": "uke", "Weeks": "uker", "Day": "dag", "Days": "dager", "Hour": "time", "Hours": "timer", "Minute": "minutt", "Minutes": "minutter", "Second": "sekund", "Seconds": "sekunder"}

@ -1 +0,0 @@
{"LanguageName": "Nyanja (Chichewa)", "LanguageCode": "ny", "Week": "sabata", "Weeks": "masabata", "Day": "tsiku", "Days": "masiku", "Hour": "ola", "Hours": "maola", "Minute": "miniti", "Minutes": "mphindi", "Second": "chachiwiri", "Seconds": "masekondi"}

@ -1 +0,0 @@
{"LanguageName": "Odia (Oriya)", "LanguageCode": "or", "Week": "\u0b38\u0b2a\u0b4d\u0b24\u0b3e\u0b39", "Weeks": "\u0b38\u0b2a\u0b4d\u0b24\u0b3e\u0b39\u0b17\u0b41\u0b21\u0b3f\u0b15 |", "Day": "\u0b26\u0b3f\u0b28", "Days": "\u0b26\u0b3f\u0b28", "Hour": "\u0b18\u0b23\u0b4d\u0b1f\u0b3e", "Hours": "\u0b18\u0b23\u0b4d\u0b1f\u0b3e", "Minute": "\u0b2e\u0b3f\u0b28\u0b3f\u0b1f\u0b4d", "Minutes": "\u0b2e\u0b3f\u0b28\u0b3f\u0b1f\u0b4d |", "Second": "\u0b26\u0b4d\u0b71\u0b3f\u0b24\u0b40\u0b5f", "Seconds": "\u0b38\u0b47\u0b15\u0b47\u0b23\u0b4d\u0b21\u0b4d |"}

@ -1 +0,0 @@
{"LanguageName": "Punjabi", "LanguageCode": "pa", "Week": "\u0a39\u0a2b\u0a3c\u0a24\u0a3e", "Weeks": "\u0a39\u0a2b\u0a3c\u0a24\u0a47", "Day": "\u0a26\u0a3f\u0a28", "Days": "\u0a26\u0a3f\u0a28", "Hour": "\u0a18\u0a70\u0a1f\u0a3e", "Hours": "\u0a18\u0a70\u0a1f\u0a47", "Minute": "\u0a2e\u0a3f\u0a70\u0a1f", "Minutes": "\u0a2e\u0a3f\u0a70\u0a1f", "Second": "\u0a26\u0a42\u0a1c\u0a3e", "Seconds": "\u0a38\u0a15\u0a3f\u0a70\u0a1f"}

@ -1 +0,0 @@
{"LanguageName": "Polish", "LanguageCode": "pl", "Week": "tydzie\u0144", "Weeks": "tygodnie", "Day": "dzie\u0144", "Days": "dni", "Hour": "godzina", "Hours": "godziny", "Minute": "minuta", "Minutes": "minuty", "Second": "drugi", "Seconds": "sekundy"}

@ -1 +0,0 @@
{"LanguageName": "Pashto", "LanguageCode": "ps", "Week": "\u0627\u0648\u0646\u06cd", "Weeks": "\u0627\u0648\u0646\u06cd", "Day": "\u0648\u0631\u0681", "Days": "\u0648\u0631\u0681\u06d0", "Hour": "\u0633\u0627\u0639\u062a", "Hours": "\u0633\u0627\u0639\u062a\u0648\u0646\u0647", "Minute": "\u062f\u0642\u06cc\u0642\u0647", "Minutes": "\u062f\u0642\u06cc\u0642\u06d0", "Second": "\u062f\u0648\u0647\u0645", "Seconds": "\u062b\u0627\u0646\u06cc\u06d0"}

@ -1 +0,0 @@
{"LanguageName": "Portuguese", "LanguageCode": "pt", "Week": "semana", "Weeks": "semanas", "Day": "dia", "Days": "dias", "Hour": "hora", "Hours": "horas", "Minute": "minuto", "Minutes": "minutos", "Second": "segundo", "Seconds": "segundos"}

@ -1 +0,0 @@
{"LanguageName": "Romanian", "LanguageCode": "ro", "Week": "s\u0103pt\u0103m\u00e2n\u0103", "Weeks": "s\u0103pt\u0103m\u00e2ni", "Day": "zi", "Days": "zile", "Hour": "ora", "Hours": "ore", "Minute": "minut", "Minutes": "minute", "Second": "al doilea", "Seconds": "secunde"}

@ -1 +0,0 @@
{"LanguageName": "Russian", "LanguageCode": "ru", "Week": "\u043d\u0435\u0434\u0435\u043b\u044f", "Weeks": "\u043d\u0435\u0434\u0435\u043b\u0438", "Day": "\u0434\u0435\u043d\u044c", "Days": "\u0434\u043d\u0438", "Hour": "\u0447\u0430\u0441", "Hours": "\u0447\u0430\u0441\u044b", "Minute": "\u043c\u0438\u043d\u0443\u0442\u0430", "Minutes": "\u043c\u0438\u043d\u0443\u0442\u044b", "Second": "\u0432\u0442\u043e\u0440\u043e\u0439", "Seconds": "\u0441\u0435\u043a\u0443\u043d\u0434\u044b"}

@ -1 +0,0 @@
{"LanguageName": "Kinyarwanda", "LanguageCode": "rw", "Week": "icyumweru", "Weeks": "ibyumweru", "Day": "umunsi", "Days": "iminsi", "Hour": "isaha", "Hours": "amasaha", "Minute": "umunota", "Minutes": "iminota", "Second": "kabiri", "Seconds": "amasegonda"}

@ -1 +0,0 @@
{"LanguageName": "Sindhi", "LanguageCode": "sd", "Week": "\u0647\u0641\u062a\u0648", "Weeks": "\u0647\u0641\u062a\u0627", "Day": "\u068f\u064a\u0646\u0647\u0646", "Days": "\u068f\u064a\u0646\u0647\u0646", "Hour": "\u06aa\u0644\u0627\u06aa", "Hours": "\u06aa\u0644\u0627\u06aa", "Minute": "\u0645\u0646\u067d", "Minutes": "\u0645\u0646\u067d", "Second": "\u067b\u064a\u0648\u0646", "Seconds": "\u0633\u064a\u06aa\u0646\u068a"}

@ -1 +0,0 @@
{"LanguageName": "Sinhala (Sinhalese)", "LanguageCode": "si", "Week": "\u0dc3\u0dad\u0dd2\u0dba", "Weeks": "\u0dc3\u0dad\u0dd2", "Day": "\u0daf\u0dc0\u0dc3", "Days": "\u0daf\u0dd2\u0db1", "Hour": "\u0db4\u0dd0\u0dba", "Hours": "\u0db4\u0dd0\u0dba", "Minute": "\u0db8\u0dd2\u0db1\u0dd2\u0dad\u0dca\u0dad\u0dd4\u0dc0", "Minutes": "\u0db8\u0dd2\u0db1\u0dd2\u0dad\u0dca\u0dad\u0dd4", "Second": "\u0daf\u0dd9\u0dc0\u0dd0\u0db1\u0dd2", "Seconds": "\u0dad\u0dad\u0dca\u0db4\u0dbb"}

@ -1 +0,0 @@
{"LanguageName": "Slovak", "LanguageCode": "sk", "Week": "t\u00fd\u017ede\u0148", "Weeks": "t\u00fd\u017ed\u0148ov", "Day": "de\u0148", "Days": "dni", "Hour": "hodina", "Hours": "hodiny", "Minute": "min\u00fatu", "Minutes": "min\u00fat", "Second": "druh\u00fd", "Seconds": "sek\u00fand"}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save