mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-28 02:54:34 +00:00
android: allow to set disappearance interval when sending message (#2455)
This commit is contained in:
@@ -160,6 +160,9 @@ dependencies {
|
||||
// Video support
|
||||
implementation "com.google.android.exoplayer:exoplayer:2.17.1"
|
||||
|
||||
// Wheel picker
|
||||
implementation 'com.github.zj565061763:compose-wheel-picker:1.0.0-alpha10'
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
||||
@@ -1715,18 +1715,18 @@ sealed class CIContent: ItemContent {
|
||||
companion object {
|
||||
fun featureText(feature: Feature, enabled: String, param: Int?): String =
|
||||
if (feature.hasParam) {
|
||||
"${feature.text}: ${TimedMessagesPreference.ttlText(param)}"
|
||||
"${feature.text}: ${timeText(param)}"
|
||||
} else {
|
||||
"${feature.text}: $enabled"
|
||||
}
|
||||
|
||||
fun preferenceText(feature: Feature, allowed: FeatureAllowed, param: Int?): String = when {
|
||||
allowed != FeatureAllowed.NO && feature.hasParam && param != null ->
|
||||
String.format(generalGetString(R.string.feature_offered_item_with_param), feature.text, TimedMessagesPreference.ttlText(param))
|
||||
String.format(generalGetString(R.string.feature_offered_item_with_param), feature.text, timeText(param))
|
||||
allowed != FeatureAllowed.NO ->
|
||||
String.format(generalGetString(R.string.feature_offered_item), feature.text, TimedMessagesPreference.ttlText(param))
|
||||
String.format(generalGetString(R.string.feature_offered_item), feature.text, timeText(param))
|
||||
else ->
|
||||
String.format(generalGetString(R.string.feature_cancelled_item), feature.text, TimedMessagesPreference.ttlText(param))
|
||||
String.format(generalGetString(R.string.feature_cancelled_item), feature.text, timeText(param))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +165,7 @@ class AppPreferences(val context: Context) {
|
||||
|
||||
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
|
||||
val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0)
|
||||
val customDisappearingMessageTime = mkIntPreference(SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME, 300)
|
||||
|
||||
private fun mkIntPreference(prefName: String, default: Int) =
|
||||
SharedPreference(
|
||||
@@ -289,6 +290,7 @@ class AppPreferences(val context: Context) {
|
||||
private const val SHARED_PREFS_THEMES = "Themes"
|
||||
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
|
||||
private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode"
|
||||
private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,8 +581,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiSendMessage(type: ChatType, id: Long, file: String? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false): AChatItem? {
|
||||
val cmd = CC.ApiSendMessage(type, id, file, quotedItemId, mc, live)
|
||||
suspend fun apiSendMessage(type: ChatType, id: Long, file: String? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false, ttl: Int? = null): AChatItem? {
|
||||
val cmd = CC.ApiSendMessage(type, id, file, quotedItemId, mc, live, ttl)
|
||||
val r = sendCmd(cmd)
|
||||
return when (r) {
|
||||
is CR.NewChatItem -> r.chatItem
|
||||
@@ -1893,7 +1895,7 @@ sealed class CC {
|
||||
class ApiStorageEncryption(val config: DBEncryptionConfig): CC()
|
||||
class ApiGetChats(val userId: Long): CC()
|
||||
class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC()
|
||||
class ApiSendMessage(val type: ChatType, val id: Long, val file: String?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean): CC()
|
||||
class ApiSendMessage(val type: ChatType, val id: Long, val file: String?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC()
|
||||
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC()
|
||||
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC()
|
||||
class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC()
|
||||
@@ -1982,7 +1984,10 @@ sealed class CC {
|
||||
is ApiStorageEncryption -> "/_db encryption ${json.encodeToString(config)}"
|
||||
is ApiGetChats -> "/_get chats $userId pcc=on"
|
||||
is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
||||
is ApiSendMessage -> "/_send ${chatRef(type, id)} live=${onOff(live)} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}"
|
||||
is ApiSendMessage -> {
|
||||
val ttlStr = if (ttl != null) "$ttl" else "default"
|
||||
"/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}"
|
||||
}
|
||||
is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}"
|
||||
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}"
|
||||
is ApiDeleteMemberChatItem -> "/_delete member item #$groupId $groupMemberId $itemId"
|
||||
@@ -2555,52 +2560,103 @@ data class TimedMessagesPreference(
|
||||
): ChatPreference {
|
||||
companion object {
|
||||
val ttlValues: List<Int?>
|
||||
get() = listOf(30, 300, 3600, 8 * 3600, 86400, 7 * 86400, 30 * 86400, null)
|
||||
get() = listOf(3600, 8 * 3600, 86400, 7 * 86400, 30 * 86400, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun ttlText(ttl: Int?): String {
|
||||
ttl ?: return generalGetString(R.string.feature_off)
|
||||
if (ttl == 0) return String.format(generalGetString(R.string.ttl_sec), 0)
|
||||
val (m_, s) = divMod(ttl, 60)
|
||||
val (h_, m) = divMod(m_, 60)
|
||||
val (d_, h) = divMod(h_, 24)
|
||||
val (mm, d) = divMod(d_, 30)
|
||||
return maybe(mm, if (mm == 1) String.format(generalGetString(R.string.ttl_month), 1) else String.format(generalGetString(R.string.ttl_months), mm)) +
|
||||
maybe(d, if (d == 1) String.format(generalGetString(R.string.ttl_day), 1) else if (d == 7) String.format(generalGetString(R.string.ttl_week), 1) else if (d == 14) String.format(generalGetString(R.string.ttl_weeks), 2) else String.format(generalGetString(R.string.ttl_days), d)) +
|
||||
maybe(h, if (h == 1) String.format(generalGetString(R.string.ttl_hour), 1) else String.format(generalGetString(R.string.ttl_hours), h)) +
|
||||
maybe(m, String.format(generalGetString(R.string.ttl_min), m)) +
|
||||
maybe(s, String.format(generalGetString(R.string.ttl_sec), s))
|
||||
sealed class CustomTimeUnit {
|
||||
object Second: CustomTimeUnit()
|
||||
object Minute: CustomTimeUnit()
|
||||
object Hour: CustomTimeUnit()
|
||||
object Day: CustomTimeUnit()
|
||||
object Week: CustomTimeUnit()
|
||||
object Month: CustomTimeUnit()
|
||||
|
||||
val toSeconds: Int
|
||||
get() =
|
||||
when (this) {
|
||||
Second -> 1
|
||||
Minute -> 60
|
||||
Hour -> 3600
|
||||
Day -> 86400
|
||||
Week -> 7 * 86400
|
||||
Month -> 30 * 86400
|
||||
}
|
||||
|
||||
val text: String
|
||||
get() =
|
||||
when (this) {
|
||||
Second -> generalGetString(R.string.custom_time_unit_seconds)
|
||||
Minute -> generalGetString(R.string.custom_time_unit_minutes)
|
||||
Hour -> generalGetString(R.string.custom_time_unit_hours)
|
||||
Day -> generalGetString(R.string.custom_time_unit_days)
|
||||
Week -> generalGetString(R.string.custom_time_unit_weeks)
|
||||
Month -> generalGetString(R.string.custom_time_unit_months)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun toTimeUnit(seconds: Int): Pair<CustomTimeUnit, Int> {
|
||||
val tryUnits = listOf(Month, Week, Day, Hour, Minute)
|
||||
var selectedUnit: Pair<CustomTimeUnit, Int>? = null
|
||||
for (unit in tryUnits) {
|
||||
val (v, r) = divMod(seconds, unit.toSeconds)
|
||||
if (r == 0) {
|
||||
selectedUnit = Pair(unit, v)
|
||||
break
|
||||
}
|
||||
}
|
||||
return selectedUnit ?: Pair(Second, seconds)
|
||||
}
|
||||
|
||||
fun shortTtlText(ttl: Int?): String {
|
||||
ttl ?: return generalGetString(R.string.feature_off)
|
||||
val m = ttl / 60
|
||||
if (m == 0) {
|
||||
return String.format(generalGetString(R.string.ttl_s), ttl)
|
||||
}
|
||||
val h = m / 60
|
||||
if (h == 0) {
|
||||
return String.format(generalGetString(R.string.ttl_m), m)
|
||||
}
|
||||
val d = h / 24
|
||||
if (d == 0) {
|
||||
return String.format(generalGetString(R.string.ttl_h), h)
|
||||
}
|
||||
val mm = d / 30
|
||||
if (mm > 0) {
|
||||
return String.format(generalGetString(R.string.ttl_mth), mm)
|
||||
}
|
||||
val w = d / 7
|
||||
return if (w == 0 || d % 7 != 0) String.format(generalGetString(R.string.ttl_d), d) else String.format(generalGetString(R.string.ttl_w), w)
|
||||
}
|
||||
|
||||
fun divMod(n: Int, d: Int): Pair<Int, Int> =
|
||||
private fun divMod(n: Int, d: Int): Pair<Int, Int> =
|
||||
n / d to n % d
|
||||
|
||||
fun maybe(n: Int, s: String): String =
|
||||
if (n == 0) "" else s
|
||||
fun toText(seconds: Int): String {
|
||||
val (unit, value) = toTimeUnit(seconds)
|
||||
return when (unit) {
|
||||
Second -> String.format(generalGetString(R.string.ttl_sec), value)
|
||||
Minute -> String.format(generalGetString(R.string.ttl_min), value)
|
||||
Hour -> if (value == 1) String.format(generalGetString(R.string.ttl_hour), 1) else String.format(generalGetString(R.string.ttl_hours), value)
|
||||
Day -> if (value == 1) String.format(generalGetString(R.string.ttl_day), 1) else String.format(generalGetString(R.string.ttl_days), value)
|
||||
Week -> if (value == 1) String.format(generalGetString(R.string.ttl_week), 1) else String.format(generalGetString(R.string.ttl_weeks), value)
|
||||
Month -> if (value == 1) String.format(generalGetString(R.string.ttl_month), 1) else String.format(generalGetString(R.string.ttl_months), value)
|
||||
}
|
||||
}
|
||||
|
||||
fun toShortText(seconds: Int): String {
|
||||
val (unit, value) = toTimeUnit(seconds)
|
||||
return when (unit) {
|
||||
Second -> String.format(generalGetString(R.string.ttl_s), value)
|
||||
Minute -> String.format(generalGetString(R.string.ttl_m), value)
|
||||
Hour -> String.format(generalGetString(R.string.ttl_h), value)
|
||||
Day -> String.format(generalGetString(R.string.ttl_d), value)
|
||||
Week -> String.format(generalGetString(R.string.ttl_w), value)
|
||||
Month -> String.format(generalGetString(R.string.ttl_mth), value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun timeText(seconds: Int?): String {
|
||||
if (seconds == null) {
|
||||
return generalGetString(R.string.feature_off)
|
||||
}
|
||||
if (seconds == 0) {
|
||||
String.format(generalGetString(R.string.ttl_sec), 0)
|
||||
}
|
||||
return CustomTimeUnit.toText(seconds)
|
||||
}
|
||||
|
||||
fun shortTimeText(seconds: Int?): String {
|
||||
if (seconds == null) {
|
||||
return generalGetString(R.string.feature_off)
|
||||
}
|
||||
if (seconds == 0) {
|
||||
String.format(generalGetString(R.string.ttl_s), 0)
|
||||
}
|
||||
return CustomTimeUnit.toShortText(seconds)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ContactUserPreferences(
|
||||
val timedMessages: ContactUserPreferenceTimed,
|
||||
|
||||
@@ -86,7 +86,7 @@ fun TerminalLayout(
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
sendMessage = sendCommand,
|
||||
sendMessage = { sendCommand() },
|
||||
sendLiveMessage = null,
|
||||
updateLiveMessage = null,
|
||||
onMessageChange = ::onMessageChange,
|
||||
|
||||
@@ -366,14 +366,15 @@ fun ComposeView(
|
||||
chatModel.filesToDelete.clear()
|
||||
}
|
||||
|
||||
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: String? = null, live: Boolean = false): ChatItem? {
|
||||
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: String? = null, live: Boolean = false, ttl: Int?): ChatItem? {
|
||||
val aChatItem = chatModel.controller.apiSendMessage(
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
file = file,
|
||||
quotedItemId = quoted,
|
||||
mc = mc,
|
||||
live = live
|
||||
live = live,
|
||||
ttl = ttl
|
||||
)
|
||||
if (aChatItem != null) chatModel.addChatItem(cInfo, aChatItem.chatItem)
|
||||
return aChatItem?.chatItem
|
||||
@@ -381,7 +382,7 @@ fun ComposeView(
|
||||
|
||||
|
||||
|
||||
suspend fun sendMessageAsync(text: String?, live: Boolean): ChatItem? {
|
||||
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? {
|
||||
val cInfo = chat.chatInfo
|
||||
val cs = composeState.value
|
||||
var sent: ChatItem?
|
||||
@@ -495,7 +496,8 @@ fun ComposeView(
|
||||
msgs.forEachIndexed { index, content ->
|
||||
if (index > 0) delay(100)
|
||||
sent = send(cInfo, content, if (index == 0) quotedItemId else null, files.getOrNull(index),
|
||||
if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false
|
||||
live = if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false,
|
||||
ttl = ttl
|
||||
)
|
||||
}
|
||||
if (sent == null &&
|
||||
@@ -503,16 +505,16 @@ fun ComposeView(
|
||||
cs.preview is ComposePreview.FilePreview ||
|
||||
cs.preview is ComposePreview.VoicePreview)
|
||||
) {
|
||||
sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live)
|
||||
sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live, ttl)
|
||||
}
|
||||
}
|
||||
clearState(live)
|
||||
return sent
|
||||
}
|
||||
|
||||
fun sendMessage() {
|
||||
fun sendMessage(ttl: Int?) {
|
||||
withBGApi {
|
||||
sendMessageAsync(null, false)
|
||||
sendMessageAsync(null, false, ttl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,7 +590,7 @@ fun ComposeView(
|
||||
val cs = composeState.value
|
||||
val typedMsg = cs.message
|
||||
if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage?.sent)) {
|
||||
val ci = sendMessageAsync(typedMsg, live = true)
|
||||
val ci = sendMessageAsync(typedMsg, live = true, ttl = null)
|
||||
if (ci != null) {
|
||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = typedMsg, sent = true))
|
||||
}
|
||||
@@ -609,7 +611,7 @@ fun ComposeView(
|
||||
if (liveMessage != null) {
|
||||
val sentMsg = liveMessageToSend(liveMessage, typedMsg)
|
||||
if (sentMsg != null) {
|
||||
val ci = sendMessageAsync(sentMsg, live = true)
|
||||
val ci = sendMessageAsync(sentMsg, live = true, ttl = null)
|
||||
if (ci != null) {
|
||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg, sent = true))
|
||||
}
|
||||
@@ -763,7 +765,7 @@ fun ComposeView(
|
||||
if (orientation == activity.resources.configuration.orientation) {
|
||||
val cs = composeState.value
|
||||
if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) {
|
||||
sendMessage()
|
||||
sendMessage(null)
|
||||
resetLinkPreview()
|
||||
clearCurrentDraft()
|
||||
deleteUnusedFiles()
|
||||
@@ -784,6 +786,9 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO in 5.2 - allow if ttl is not configured
|
||||
// val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) }
|
||||
val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) && chat.chatInfo.timedMessagesTTL != null }
|
||||
SendMsgView(
|
||||
composeState,
|
||||
showVoiceRecordIcon = true,
|
||||
@@ -795,8 +800,10 @@ fun ComposeView(
|
||||
allowVoiceToContact = ::allowVoiceToContact,
|
||||
userIsObserver = userIsObserver.value,
|
||||
userCanSend = userCanSend.value,
|
||||
sendMessage = {
|
||||
sendMessage()
|
||||
timedMessageAllowed = timedMessageAllowed,
|
||||
customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime,
|
||||
sendMessage = { ttl ->
|
||||
sendMessage(ttl)
|
||||
resetLinkPreview()
|
||||
},
|
||||
sendLiveMessage = ::sendLiveMessage,
|
||||
|
||||
@@ -188,7 +188,7 @@ private fun TimedMessagesFeatureSection(
|
||||
val ttl = rememberSaveable(featuresAllowed.timedMessagesTTL) { mutableStateOf(featuresAllowed.timedMessagesTTL) }
|
||||
TimedMessagesTTLPicker(ttl, onTTLUpdated)
|
||||
} else if (pref.contactPreference.allow == FeatureAllowed.YES || pref.contactPreference.allow == FeatureAllowed.ALWAYS) {
|
||||
InfoRow(generalGetString(R.string.delete_after), TimedMessagesPreference.ttlText(pref.contactPreference.ttl))
|
||||
InfoRow(generalGetString(R.string.delete_after), timeText(pref.contactPreference.ttl))
|
||||
}
|
||||
}
|
||||
SectionTextFooter(ChatFeature.TimedMessages.enabledDescription(enabled))
|
||||
@@ -212,7 +212,7 @@ fun TimedMessagesTTLPicker(selection: MutableState<Int?>, onSelected: (Int?) ->
|
||||
val values = ttlValues + if (ttlValues.contains(selection.value)) listOf() else listOf(selection.value)
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(R.string.delete_after),
|
||||
values.map { it to TimedMessagesPreference.ttlText(it) },
|
||||
values.map { it to timeText(it) },
|
||||
selection,
|
||||
onSelected = onSelected
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.*
|
||||
@@ -29,14 +29,14 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.*
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.*
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||
@@ -44,8 +44,7 @@ import androidx.core.widget.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.CurrentColors
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.item.ItemAction
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
@@ -64,13 +63,25 @@ fun SendMsgView(
|
||||
userIsObserver: Boolean,
|
||||
userCanSend: Boolean,
|
||||
allowVoiceToContact: () -> Unit,
|
||||
sendMessage: () -> Unit,
|
||||
timedMessageAllowed: Boolean = false,
|
||||
customDisappearingMessageTimePref: SharedPreference<Int>? = null,
|
||||
sendMessage: (Int?) -> Unit,
|
||||
sendLiveMessage: (suspend () -> Unit)? = null,
|
||||
updateLiveMessage: (suspend () -> Unit)? = null,
|
||||
cancelLiveMessage: (() -> Unit)? = null,
|
||||
onMessageChange: (String) -> Unit,
|
||||
textStyle: MutableState<TextStyle>
|
||||
) {
|
||||
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
|
||||
|
||||
if (showCustomDisappearingMessageDialog.value) {
|
||||
CustomDisappearingMessageDialog(
|
||||
sendMessage = sendMessage,
|
||||
setShowDialog = { showCustomDisappearingMessageDialog.value = it },
|
||||
customDisappearingMessageTimePref = customDisappearingMessageTimePref
|
||||
)
|
||||
}
|
||||
|
||||
Box(Modifier.padding(vertical = 8.dp)) {
|
||||
val cs = composeState.value
|
||||
val showProgress = cs.inProgress && (cs.preview is ComposePreview.MediaPreview || cs.preview is ComposePreview.FilePreview)
|
||||
@@ -80,14 +91,15 @@ fun SendMsgView(
|
||||
NativeKeyboard(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange)
|
||||
// Disable clicks on text field
|
||||
if (cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) {
|
||||
Box(Modifier
|
||||
.matchParentSize()
|
||||
.clickable(enabled = !userCanSend, indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.observer_cant_send_message_title),
|
||||
text = generalGetString(R.string.observer_cant_send_message_desc)
|
||||
)
|
||||
})
|
||||
Box(
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.clickable(enabled = !userCanSend, indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.observer_cant_send_message_title),
|
||||
text = generalGetString(R.string.observer_cant_send_message_desc)
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
if (showDeleteTextButton.value) {
|
||||
@@ -124,10 +136,11 @@ fun SendMsgView(
|
||||
else ->
|
||||
RecordVoiceView(recState, stopRecOnNextClick)
|
||||
}
|
||||
if (sendLiveMessage != null
|
||||
&& updateLiveMessage != null
|
||||
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
|
||||
&& cs.contextItem is ComposeContextItem.NoContextItem) {
|
||||
if (sendLiveMessage != null
|
||||
&& updateLiveMessage != null
|
||||
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
|
||||
&& cs.contextItem is ComposeContextItem.NoContextItem
|
||||
) {
|
||||
Spacer(Modifier.width(10.dp))
|
||||
StartLiveMessageButton(userCanSend) {
|
||||
if (composeState.value.preview is ComposePreview.NoPreview) {
|
||||
@@ -146,27 +159,43 @@ fun SendMsgView(
|
||||
val cs = composeState.value
|
||||
val icon = if (cs.editing || cs.liveMessage != null) painterResource(R.drawable.ic_check_filled) else painterResource(R.drawable.ic_arrow_upward)
|
||||
val disabled = !cs.sendEnabled() ||
|
||||
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
|
||||
cs.endLiveDisabled
|
||||
if (cs.liveMessage == null &&
|
||||
cs.preview !is ComposePreview.VoicePreview && !cs.editing &&
|
||||
cs.contextItem is ComposeContextItem.NoContextItem &&
|
||||
sendLiveMessage != null && updateLiveMessage != null
|
||||
) {
|
||||
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
|
||||
cs.endLiveDisabled
|
||||
val showSendLiveMessageMenuButton =
|
||||
cs.liveMessage == null && !cs.editing &&
|
||||
cs.preview !is ComposePreview.VoicePreview &&
|
||||
cs.contextItem is ComposeContextItem.NoContextItem &&
|
||||
sendLiveMessage != null && updateLiveMessage != null
|
||||
val showSendDisappearingMessageMenuButton =
|
||||
cs.liveMessage == null && !cs.editing &&
|
||||
timedMessageAllowed
|
||||
|
||||
if (showSendLiveMessageMenuButton || showSendDisappearingMessageMenuButton) {
|
||||
val showDropdown = rememberSaveable { mutableStateOf(false) }
|
||||
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown.value = true }
|
||||
|
||||
DefaultDropdownMenu(
|
||||
showDropdown,
|
||||
) {
|
||||
ItemAction(
|
||||
generalGetString(R.string.send_live_message),
|
||||
BoltFilled,
|
||||
onClick = {
|
||||
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
|
||||
showDropdown.value = false
|
||||
}
|
||||
)
|
||||
if (showSendLiveMessageMenuButton && sendLiveMessage != null && updateLiveMessage != null) {
|
||||
ItemAction(
|
||||
generalGetString(R.string.send_live_message),
|
||||
BoltFilled,
|
||||
onClick = {
|
||||
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
|
||||
showDropdown.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
if (showSendDisappearingMessageMenuButton) {
|
||||
ItemAction(
|
||||
generalGetString(R.string.disappearing_message),
|
||||
painterResource(R.drawable.ic_timer),
|
||||
onClick = {
|
||||
showCustomDisappearingMessageDialog.value = true
|
||||
showDropdown.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage)
|
||||
@@ -177,6 +206,99 @@ fun SendMsgView(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CustomDisappearingMessageDialog(
|
||||
sendMessage: (Int?) -> Unit,
|
||||
setShowDialog: (Boolean) -> Unit,
|
||||
customDisappearingMessageTimePref: SharedPreference<Int>?
|
||||
) {
|
||||
val showCustomTimePicker = remember { mutableStateOf(false) }
|
||||
|
||||
if (showCustomTimePicker.value) {
|
||||
val selectedDisappearingMessageTime = remember {
|
||||
mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300)
|
||||
}
|
||||
CustomTimePickerDialog(
|
||||
selectedDisappearingMessageTime,
|
||||
title = generalGetString(R.string.delete_after),
|
||||
confirmButtonText = generalGetString(R.string.send_disappearing_message_send),
|
||||
confirmButtonAction = { ttl ->
|
||||
sendMessage(ttl)
|
||||
customDisappearingMessageTimePref?.set?.invoke(ttl)
|
||||
setShowDialog(false)
|
||||
},
|
||||
cancel = { setShowDialog(false) }
|
||||
)
|
||||
} else {
|
||||
@Composable
|
||||
fun ChoiceButton(
|
||||
text: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
TextButton(onClick) {
|
||||
Text(
|
||||
text,
|
||||
fontSize = 18.sp,
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(onDismissRequest = { setShowDialog(false) }) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(DEFAULT_PADDING),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(" ") // centers title
|
||||
Text(
|
||||
generalGetString(R.string.send_disappearing_message),
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_close),
|
||||
generalGetString(R.string.icon_descr_close_button),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.size(25.dp)
|
||||
.clickable { setShowDialog(false) }
|
||||
)
|
||||
}
|
||||
|
||||
ChoiceButton(generalGetString(R.string.send_disappearing_message_30_seconds)) {
|
||||
sendMessage(30)
|
||||
setShowDialog(false)
|
||||
}
|
||||
ChoiceButton(generalGetString(R.string.send_disappearing_message_1_minute)) {
|
||||
sendMessage(60)
|
||||
setShowDialog(false)
|
||||
}
|
||||
ChoiceButton(generalGetString(R.string.send_disappearing_message_5_minutes)) {
|
||||
sendMessage(300)
|
||||
setShowDialog(false)
|
||||
}
|
||||
ChoiceButton(generalGetString(R.string.send_disappearing_message_custom_time)) {
|
||||
showCustomTimePicker.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NativeKeyboard(
|
||||
composeState: MutableState<ComposeState>,
|
||||
@@ -455,14 +577,14 @@ private fun SendMsgButton(
|
||||
sizeDp: Animatable<Float, AnimationVector1D>,
|
||||
alpha: Animatable<Float, AnimationVector1D>,
|
||||
enabled: Boolean,
|
||||
sendMessage: () -> Unit,
|
||||
sendMessage: (Int?) -> Unit,
|
||||
onLongClick: (() -> Unit)? = null
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Box(
|
||||
modifier = Modifier.requiredSize(36.dp)
|
||||
.combinedClickable(
|
||||
onClick = sendMessage,
|
||||
onClick = { sendMessage(null) },
|
||||
onLongClick = onLongClick,
|
||||
enabled = enabled,
|
||||
role = Role.Button,
|
||||
@@ -607,6 +729,7 @@ fun PreviewSendMsgView() {
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
timedMessageAllowed = false,
|
||||
sendMessage = {},
|
||||
onMessageChange = { _ -> },
|
||||
textStyle = textStyle
|
||||
@@ -637,6 +760,7 @@ fun PreviewSendMsgViewEditing() {
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
timedMessageAllowed = false,
|
||||
sendMessage = {},
|
||||
onMessageChange = { _ -> },
|
||||
textStyle = textStyle
|
||||
@@ -667,6 +791,7 @@ fun PreviewSendMsgViewInProgress() {
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
timedMessageAllowed = false,
|
||||
sendMessage = {},
|
||||
onMessageChange = { _ -> },
|
||||
textStyle = textStyle
|
||||
|
||||
+1
-1
@@ -151,7 +151,7 @@ private fun FeatureSection(
|
||||
iconTint = iconTint,
|
||||
)
|
||||
if (timedOn) {
|
||||
InfoRow(generalGetString(R.string.delete_after), TimedMessagesPreference.ttlText(preferences.timedMessages.ttl))
|
||||
InfoRow(generalGetString(R.string.delete_after), timeText(preferences.timedMessages.ttl))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
|
||||
StatusIconText(painterResource(R.drawable.ic_timer), color)
|
||||
val ttl = meta.itemTimed?.ttl
|
||||
if (ttl != chatTTL) {
|
||||
Text(TimedMessagesPreference.shortTtlText(ttl), color = color, fontSize = 13.sp)
|
||||
Text(shortTimeText(ttl), color = color, fontSize = 13.sp)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
}
|
||||
@@ -69,7 +69,7 @@ fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String {
|
||||
res += iconSpace
|
||||
val ttl = meta.itemTimed.ttl
|
||||
if (ttl != chatTTL) {
|
||||
res += TimedMessagesPreference.shortTtlText(ttl)
|
||||
res += shortTimeText(ttl)
|
||||
}
|
||||
}
|
||||
if (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing) {
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.CustomTimeUnit
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import com.sd.lib.compose.wheel_picker.*
|
||||
|
||||
@Composable
|
||||
fun CustomTimePicker(
|
||||
selection: MutableState<Int>,
|
||||
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits
|
||||
) {
|
||||
fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List<Int> {
|
||||
val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit)
|
||||
val unitValues = (unitLimits.minValue..unitLimits.maxValue).toList()
|
||||
return unitValues + if (unitValues.contains(selectedValue)) emptyList() else listOf(selectedValue)
|
||||
}
|
||||
|
||||
val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value)
|
||||
val selectedUnit: MutableState<CustomTimeUnit> = remember { mutableStateOf(unit) }
|
||||
val selectedDuration = remember { mutableStateOf(duration) }
|
||||
val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) }
|
||||
|
||||
LaunchedEffect(selectedUnit.value) {
|
||||
val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue
|
||||
if (maxValue != null && selectedDuration.value > maxValue) {
|
||||
selectedDuration.value = maxValue
|
||||
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
|
||||
} else {
|
||||
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
|
||||
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(selectedDuration.value) {
|
||||
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.spacedBy(0.dp)
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value))
|
||||
FVerticalWheelPicker(
|
||||
count = selectedUnitValues.value.count(),
|
||||
state = durationPickerState,
|
||||
unfocusedCount = 2,
|
||||
focus = {
|
||||
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
|
||||
}
|
||||
) { index ->
|
||||
Text(
|
||||
selectedUnitValues.value[index].toString(),
|
||||
fontSize = 18.sp,
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
LaunchedEffect(durationPickerState) {
|
||||
snapshotFlow { durationPickerState.currentIndex }
|
||||
.collect {
|
||||
selectedDuration.value = selectedUnitValues.value[it]
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(Modifier.weight(1f)) {
|
||||
val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value })
|
||||
FVerticalWheelPicker(
|
||||
count = timeUnitsLimits.count(),
|
||||
state = unitPickerState,
|
||||
unfocusedCount = 2,
|
||||
focus = {
|
||||
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
|
||||
}
|
||||
) { index ->
|
||||
Text(
|
||||
timeUnitsLimits[index].timeUnit.text,
|
||||
fontSize = 18.sp,
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
LaunchedEffect(unitPickerState) {
|
||||
snapshotFlow { unitPickerState.currentIndex }
|
||||
.collect {
|
||||
selectedUnit.value = timeUnitsLimits[it].timeUnit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class TimeUnitLimits(
|
||||
val timeUnit: CustomTimeUnit,
|
||||
val minValue: Int = 1,
|
||||
val maxValue: Int
|
||||
) {
|
||||
companion object {
|
||||
fun defaultUnitLimits(unit: CustomTimeUnit): TimeUnitLimits {
|
||||
return when (unit) {
|
||||
CustomTimeUnit.Second -> TimeUnitLimits(CustomTimeUnit.Second, maxValue = 120)
|
||||
CustomTimeUnit.Minute -> TimeUnitLimits(CustomTimeUnit.Minute, maxValue = 120)
|
||||
CustomTimeUnit.Hour -> TimeUnitLimits(CustomTimeUnit.Hour, maxValue = 72)
|
||||
CustomTimeUnit.Day -> TimeUnitLimits(CustomTimeUnit.Day, maxValue = 60)
|
||||
CustomTimeUnit.Week -> TimeUnitLimits(CustomTimeUnit.Week, maxValue = 12) // TODO in 5.2 - 54
|
||||
CustomTimeUnit.Month -> TimeUnitLimits(CustomTimeUnit.Month, maxValue = 3) // TODO in 5.2 - 12
|
||||
}
|
||||
}
|
||||
|
||||
val defaultUnitsLimits: List<TimeUnitLimits>
|
||||
get() = listOf(
|
||||
defaultUnitLimits(CustomTimeUnit.Second),
|
||||
defaultUnitLimits(CustomTimeUnit.Minute),
|
||||
defaultUnitLimits(CustomTimeUnit.Hour),
|
||||
defaultUnitLimits(CustomTimeUnit.Day),
|
||||
defaultUnitLimits(CustomTimeUnit.Week),
|
||||
defaultUnitLimits(CustomTimeUnit.Month)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CustomTimePickerDialog(
|
||||
selection: MutableState<Int>,
|
||||
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits,
|
||||
title: String,
|
||||
confirmButtonText: String,
|
||||
confirmButtonAction: (Int) -> Unit,
|
||||
cancel: () -> Unit
|
||||
) {
|
||||
Dialog(onDismissRequest = cancel) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(DEFAULT_PADDING),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(" ") // centers title
|
||||
Text(
|
||||
title,
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_close),
|
||||
generalGetString(R.string.icon_descr_close_button),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.size(25.dp)
|
||||
.clickable { cancel() }
|
||||
)
|
||||
}
|
||||
|
||||
CustomTimePicker(
|
||||
selection,
|
||||
timeUnitsLimits
|
||||
)
|
||||
|
||||
TextButton(onClick = { confirmButtonAction(selection.value) }) {
|
||||
Text(
|
||||
confirmButtonText,
|
||||
fontSize = 18.sp,
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,5 +5,5 @@
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="m480,520.5 l-209,209q-9,9 -20.25,9t-20.25,-9q-9,-9 -9,-20.25t9,-20.25L440,479.5 230.5,270q-9,-8.5 -9,-19.75t9,-20.25q9,-9 20.25,-9t20.25,9l209,209.5 209,-209q9,-9 20.25,-9t20.25,9q9,9 9,20.25t-9,20.25l-209,209L730,689.5q8.5,9 8.5,20.25T730,729.5q-9,9 -20.25,9t-20.25,-9L480,520.5Z"/>
|
||||
android:pathData="M480,520.5L271,729.5Q262,738.5 250.75,738.5Q239.5,738.5 230.5,729.5Q221.5,720.5 221.5,709.25Q221.5,698 230.5,689L440,479.5L230.5,270Q221.5,261.5 221.5,250.25Q221.5,239 230.5,230Q239.5,221 250.75,221Q262,221 271,230L480,439.5L689,230.5Q698,221.5 709.25,221.5Q720.5,221.5 729.5,230.5Q738.5,239.5 738.5,250.75Q738.5,262 729.5,271L520.5,480L730,689.5Q738.5,698.5 738.5,709.75Q738.5,721 730,729.5Q721,738.5 709.75,738.5Q698.5,738.5 689.5,729.5L480,520.5Z"/>
|
||||
</vector>
|
||||
|
||||
@@ -338,6 +338,13 @@
|
||||
<string name="ask_your_contact_to_enable_voice">Please ask your contact to enable sending voice messages.</string>
|
||||
<string name="only_group_owners_can_enable_voice">Only group owners can enable voice messages.</string>
|
||||
<string name="send_live_message">Send live message</string>
|
||||
<string name="disappearing_message">Disappearing message</string>
|
||||
<string name="send_disappearing_message">Send disappearing message</string>
|
||||
<string name="send_disappearing_message_30_seconds">30 seconds</string>
|
||||
<string name="send_disappearing_message_1_minute">1 minute</string>
|
||||
<string name="send_disappearing_message_5_minutes">5 minutes</string>
|
||||
<string name="send_disappearing_message_custom_time">Custom time</string>
|
||||
<string name="send_disappearing_message_send">Send</string>
|
||||
<string name="live_message">Live message!</string>
|
||||
<string name="send_live_message_desc">Send a live message - it will update for the recipient(s) as you type it</string>
|
||||
<string name="send_verb">Send</string>
|
||||
@@ -1392,4 +1399,12 @@
|
||||
<string name="v5_0_app_passcode_descr">Set it instead of system authentication.</string>
|
||||
<string name="v5_0_polish_interface">Polish interface</string>
|
||||
<string name="v5_0_polish_interface_descr">Thanks to the users – contribute via Weblate!</string>
|
||||
|
||||
<!-- CustomTimePicker -->
|
||||
<string name="custom_time_unit_seconds">seconds</string>
|
||||
<string name="custom_time_unit_minutes">minutes</string>
|
||||
<string name="custom_time_unit_hours">hours</string>
|
||||
<string name="custom_time_unit_days">days</string>
|
||||
<string name="custom_time_unit_weeks">weeks</string>
|
||||
<string name="custom_time_unit_months">months</string>
|
||||
</resources>
|
||||
|
||||
@@ -10,6 +10,7 @@ dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
}
|
||||
rootProject.name = "SimpleX"
|
||||
|
||||
Reference in New Issue
Block a user