android: delivery receipts (#2705)

* android: delivery receipts

* receipts toggle in privacy and in contact info (crashes), double tick in chat

* double tick image

* SetDeliveryReceiptsView

* small changes

* update users

* remove import

* fixing crash

* prevent ConcurrentModificationException

* fix enable all users

* android enable all users

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
This commit is contained in:
Evgeny Poberezkin
2023-07-17 12:24:52 +01:00
committed by GitHub
parent 1bd8f66730
commit d3b5bbe566
13 changed files with 420 additions and 18 deletions

View File

@@ -1133,7 +1133,6 @@ func startChat(refreshInvitations: Bool = true) throws {
m.onboardingStage = [.step1_SimpleXInfo, .step2_CreateProfile].contains(savedOnboardingStage) && m.users.count == 1
? .step3_CreateSimpleXAddress
: savedOnboardingStage
// TODO don't show on first start
if m.onboardingStage == .onboardingComplete && !privacyDeliveryReceiptsSet.get() {
m.setDeliveryReceipts = true
}

View File

@@ -25,10 +25,21 @@ struct SetDeliveryReceiptsView: View {
Button("Enable") {
Task {
do {
try await apiSetAllContactReceipts(enable: true)
await MainActor.run {
m.setDeliveryReceipts = false
privacyDeliveryReceiptsSet.set(true)
if let currentUser = m.currentUser {
try await apiSetAllContactReceipts(enable: true)
await MainActor.run {
var updatedUser = currentUser
updatedUser.sendRcptsContacts = true
m.updateUser(updatedUser)
m.setDeliveryReceipts = false
privacyDeliveryReceiptsSet.set(true)
}
do {
let users = try await listUsersAsync()
await MainActor.run { m.users = users }
} catch let error {
logger.debug("listUsers error: \(responseError(error))")
}
}
} catch let error {
AlertManager.shared.showAlert(Alert(

View File

@@ -16,8 +16,6 @@ public struct User: Decodable, NamedChat, Identifiable {
public var profile: LocalProfile
public var fullPreferences: FullPreferences
public var activeUser: Bool
public var sendRcptsContacts: Bool
public var sendRcptsSmallGroups: Bool
public var displayName: String { get { profile.displayName } }
public var fullName: String { get { profile.fullName } }
@@ -25,6 +23,8 @@ public struct User: Decodable, NamedChat, Identifiable {
public var localAlias: String { get { "" } }
public var showNtfs: Bool
public var sendRcptsContacts: Bool
public var sendRcptsSmallGroups: Bool
public var viewPwdHash: UserPwdHash?
public var id: Int64 { userId }
@@ -46,9 +46,9 @@ public struct User: Decodable, NamedChat, Identifiable {
profile: LocalProfile.sampleData,
fullPreferences: FullPreferences.sampleData,
activeUser: true,
showNtfs: true,
sendRcptsContacts: true,
sendRcptsSmallGroups: false,
showNtfs: true
sendRcptsSmallGroups: false
)
}

View File

@@ -37,6 +37,7 @@ import chat.simplex.app.views.localauth.SetAppPasscodeView
import chat.simplex.app.views.newchat.*
import chat.simplex.app.views.onboarding.*
import chat.simplex.app.views.usersettings.LAMode
import chat.simplex.app.views.usersettings.SetDeliveryReceiptsView
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -469,11 +470,15 @@ fun MainPage(
translationX = -offset.value.dp.toPx()
}
) {
if (chatModel.setDeliveryReceipts.value) {
SetDeliveryReceiptsView(chatModel)
} else {
val stopped = chatModel.chatRunning.value == false
if (chatModel.sharedContent.value == null)
ChatListView(chatModel, setPerformLA, stopped)
else
ShareListView(chatModel, stopped)
}
}
val scope = rememberCoroutineScope()
val onComposed: () -> Unit = {

View File

@@ -71,6 +71,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
val user = chatController.apiGetActiveUser()
if (user == null) {
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
chatModel.currentUser.value = null
chatModel.users.clear()
@@ -81,6 +82,9 @@ class SimplexApp: Application(), LifecycleEventObserver {
} else {
savedOnboardingStage
}
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) {
chatModel.setDeliveryReceipts.value = true
}
chatController.startChat(user)
// Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {

View File

@@ -39,6 +39,7 @@ import kotlin.time.*
object ChatModel {
val controller: ChatController = ChatController
val onboardingStage = mutableStateOf<OnboardingStage?>(null)
val setDeliveryReceipts = mutableStateOf(false)
val currentUser = mutableStateOf<User?>(null)
val users = mutableStateListOf<UserInfo>()
val userCreated = mutableStateOf<Boolean?>(null)
@@ -498,6 +499,8 @@ data class User(
val fullPreferences: FullChatPreferences,
val activeUser: Boolean,
val showNtfs: Boolean,
val sendRcptsContacts: Boolean,
val sendRcptsSmallGroups: Boolean,
val viewPwdHash: UserPwdHash?
): NamedChat {
override val displayName: String get() = profile.displayName
@@ -520,6 +523,8 @@ data class User(
fullPreferences = FullChatPreferences.sampleData,
activeUser = true,
showNtfs = true,
sendRcptsContacts = true,
sendRcptsSmallGroups = false,
viewPwdHash = null,
)
}
@@ -825,7 +830,7 @@ data class Contact(
profile = LocalProfile.sampleData,
activeConn = Connection.sampleData,
contactUsed = true,
chatSettings = ChatSettings(true, false),
chatSettings = ChatSettings(enableNtfs = true, sendRcpts = null, favorite = false),
userPreferences = ChatPreferences.sampleData,
mergedPreferences = ContactUserPreferences.sampleData,
createdAt = Clock.System.now(),
@@ -974,7 +979,7 @@ data class GroupInfo (
fullGroupPreferences = FullGroupPreferences.sampleData,
membership = GroupMember.sampleData,
hostConnCustomUserProfileId = null,
chatSettings = ChatSettings(true, false),
chatSettings = ChatSettings(enableNtfs = true, sendRcpts = null, favorite = false),
createdAt = Clock.System.now(),
updatedAt = Clock.System.now()
)
@@ -1614,6 +1619,10 @@ data class CIMeta (
fun statusIcon(primaryColor: Color, metaColor: Color = CurrentColors.value.colors.secondary): Pair<ImageResource, Color>? =
when (itemStatus) {
is CIStatus.SndSent -> MR.images.ic_check_filled to metaColor
is CIStatus.SndRcvd -> when(itemStatus.msgRcptStatus) {
MsgReceiptStatus.Ok -> MR.images.ic_double_check to metaColor
MsgReceiptStatus.BadMsgHash -> MR.images.ic_double_check to Color.Red
}
is CIStatus.SndErrorAuth -> MR.images.ic_close to Color.Red
is CIStatus.SndError -> MR.images.ic_warning_filled to WarningYellow
is CIStatus.RcvNew -> MR.images.ic_circle_filled to primaryColor
@@ -1698,12 +1707,19 @@ fun localTimestamp(t: Instant): String {
sealed class CIStatus {
@Serializable @SerialName("sndNew") class SndNew: CIStatus()
@Serializable @SerialName("sndSent") class SndSent: CIStatus()
@Serializable @SerialName("sndRcvd") class SndRcvd(val msgRcptStatus: MsgReceiptStatus): CIStatus()
@Serializable @SerialName("sndErrorAuth") class SndErrorAuth: CIStatus()
@Serializable @SerialName("sndError") class SndError(val agentError: String): CIStatus()
@Serializable @SerialName("rcvNew") class RcvNew: CIStatus()
@Serializable @SerialName("rcvRead") class RcvRead: CIStatus()
}
@Serializable
enum class MsgReceiptStatus {
@SerialName("ok") Ok,
@SerialName("badMsgHash") BadMsgHash;
}
@Serializable
sealed class CIDeleted {
@Serializable @SerialName("deleted") class Deleted(val deletedTs: Instant?): CIDeleted()

View File

@@ -93,6 +93,7 @@ class AppPreferences {
},
set = fun(mode: SimplexLinkMode) { _simplexLinkMode.set(mode.name) }
)
val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false)
val privacyFullBackup = mkBoolPreference(SHARED_PREFS_PRIVACY_FULL_BACKUP, false)
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false)
@@ -238,6 +239,7 @@ class AppPreferences {
private const val SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE = "PrivacyTransferImagesInline"
private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews"
private const val SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode"
private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet"
internal const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites"
@@ -461,6 +463,18 @@ object ChatController {
throw Exception("failed to set the user as active ${r.responseType} ${r.details}")
}
suspend fun apiSetAllContactReceipts(enable: Boolean) {
val r = sendCmd(CC.SetAllContactReceipts(enable))
if (r is CR.CmdOk) return
throw Exception("failed to enable receipts for all users ${r.responseType} ${r.details}")
}
suspend fun apiSetUserContactReceipts(userId: Long, userMsgReceiptSettings: UserMsgReceiptSettings) {
val r = sendCmd(CC.ApiSetUserContactReceipts(userId, userMsgReceiptSettings))
if (r is CR.CmdOk) return
throw Exception("failed to enable receipts for user contacts ${r.responseType} ${r.details}")
}
suspend fun apiHideUser(userId: Long, viewPwd: String): User =
setUserPrivacy(CC.ApiHideUser(userId, viewPwd))
@@ -699,7 +713,7 @@ object ChatController {
}
}
suspend fun apiSetSettings(type: ChatType,id: Long, settings: ChatSettings): Boolean {
suspend fun apiSetSettings(type: ChatType, id: Long, settings: ChatSettings): Boolean {
val r = sendCmd(CC.APISetChatSettings(type, id, settings))
return when (r) {
is CR.CmdOk -> true
@@ -1762,6 +1776,8 @@ sealed class CC {
class CreateActiveUser(val profile: Profile?, val sameServers: Boolean, val pastTimestamp: Boolean): CC()
class ListUsers: CC()
class ApiSetActiveUser(val userId: Long, val viewPwd: String?): CC()
class SetAllContactReceipts(val enable: Boolean): CC()
class ApiSetUserContactReceipts(val userId: Long, val userMsgReceiptSettings: UserMsgReceiptSettings): CC()
class ApiHideUser(val userId: Long, val viewPwd: String): CC()
class ApiUnhideUser(val userId: Long, val viewPwd: String): CC()
class ApiMuteUser(val userId: Long): CC()
@@ -1856,6 +1872,11 @@ sealed class CC {
}
is ListUsers -> "/users"
is ApiSetActiveUser -> "/_user $userId${maybePwd(viewPwd)}"
is SetAllContactReceipts -> "/set receipts all ${onOff(enable)}"
is ApiSetUserContactReceipts -> {
val mrs = userMsgReceiptSettings
"/_set receipts $userId ${onOff(mrs.enable)} clear_overrides=${onOff(mrs.clearOverrides)}"
}
is ApiHideUser -> "/_hide user $userId ${json.encodeToString(viewPwd)}"
is ApiUnhideUser -> "/_unhide user $userId ${json.encodeToString(viewPwd)}"
is ApiMuteUser -> "/_mute user $userId"
@@ -1951,6 +1972,8 @@ sealed class CC {
is CreateActiveUser -> "createActiveUser"
is ListUsers -> "listUsers"
is ApiSetActiveUser -> "apiSetActiveUser"
is SetAllContactReceipts -> "setAllContactReceipts"
is ApiSetUserContactReceipts -> "apiSetUserContactReceipts"
is ApiHideUser -> "apiHideUser"
is ApiUnhideUser -> "apiUnhideUser"
is ApiMuteUser -> "apiMuteUser"
@@ -2389,13 +2412,17 @@ data class KeepAliveOpts(
@Serializable
data class ChatSettings(
val enableNtfs: Boolean,
val sendRcpts: Boolean?,
val favorite: Boolean
) {
companion object {
val defaults: ChatSettings = ChatSettings(enableNtfs = true, favorite = false)
val defaults: ChatSettings = ChatSettings(enableNtfs = true, sendRcpts = null, favorite = false)
}
}
@Serializable
data class UserMsgReceiptSettings(val enable: Boolean, val clearOverrides: Boolean)
@Serializable
data class FullChatPreferences(
val timedMessages: TimedMessagesPreference,

View File

@@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chatlist.updateChatSettings
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
import chat.simplex.app.views.usersettings.*
@@ -52,15 +53,26 @@ fun ChatInfoView(
) {
BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val currentUser = chatModel.currentUser.value
val connStats = remember { mutableStateOf(connectionStats) }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
if (chat != null) {
if (chat != null && currentUser != null) {
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap()) {
mutableStateOf(chatModel.contactNetworkStatus(contact))
}
val sendReceipts = remember { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
ChatInfoLayout(
chat,
contact,
currentUser,
sendReceipts = sendReceipts,
setSendReceipts = { sendRcpts ->
withApi {
val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool)
updateChatSettings(chat, chatSettings, chatModel)
sendReceipts.value = sendRcpts
}
},
connStats = connStats,
contactNetworkStatus.value,
customUserProfile,
@@ -154,6 +166,34 @@ fun ChatInfoView(
}
}
sealed class SendReceipts {
object Yes: SendReceipts()
object No: SendReceipts()
data class UserDefault(val enable: Boolean): SendReceipts()
val text: String get() = when (this) {
is Yes -> generalGetString(MR.strings.chat_preferences_yes)
is No -> generalGetString(MR.strings.chat_preferences_no)
is UserDefault -> String.format(
generalGetString(MR.strings.chat_preferences_default),
generalGetString(if (enable) MR.strings.chat_preferences_yes else MR.strings.chat_preferences_no)
)
}
val bool: Boolean? get() = when (this) {
is Yes -> true
is No -> false
is UserDefault -> null
}
companion object {
fun fromBool(enable: Boolean?, userDefault: Boolean): SendReceipts {
return if (enable == null) UserDefault(userDefault)
else if (enable) Yes else No
}
}
}
fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.delete_contact_question),
@@ -197,6 +237,9 @@ fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit
fun ChatInfoLayout(
chat: Chat,
contact: Contact,
currentUser: User,
sendReceipts: State<SendReceipts>,
setSendReceipts: (SendReceipts) -> Unit,
connStats: MutableState<ConnectionStats?>,
contactNetworkStatus: NetworkStatus,
customUserProfile: Profile?,
@@ -240,6 +283,7 @@ fun ChatInfoLayout(
VerifyCodeButton(contact.verified, verifyClicked)
}
ContactPreferencesButton(openPreferences)
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
if (cStats != null && cStats.ratchetSyncAllowed) {
SynchronizeConnectionButton(syncContactConnection)
} else if (developerTools) {
@@ -499,6 +543,21 @@ private fun ContactPreferencesButton(onClick: () -> Unit) {
)
}
@Composable
private fun SendReceiptsOption(currentUser: User, state: State<SendReceipts>, onSelected: (SendReceipts) -> Unit) {
val values = remember {
mutableListOf(SendReceipts.Yes, SendReceipts.No, SendReceipts.UserDefault(currentUser.sendRcptsContacts)).map { it to it.text }
}
ExposedDropDownSettingRow(
generalGetString(MR.strings.send_receipts),
values,
state,
icon = painterResource(MR.images.ic_double_check),
enabled = remember { mutableStateOf(true) },
onSelected = onSelected
)
}
@Composable
fun ClearChatButton(onClick: () -> Unit) {
SettingsActionItem(
@@ -577,6 +636,9 @@ fun PreviewChatInfoLayout() {
chatItems = arrayListOf()
),
Contact.sampleData,
User.sampleData,
sendReceipts = remember { mutableStateOf(SendReceipts.Yes) },
setSendReceipts = {},
localAlias = "",
connectionCode = "123",
developerTools = false,

View File

@@ -10,8 +10,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.*
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.CurrentColors
import chat.simplex.res.MR
@@ -51,7 +50,11 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color)
if (statusIcon != null) {
val (icon, statusColor) = statusIcon
StatusIconText(painterResource(icon), statusColor)
if (meta.itemStatus is CIStatus.SndSent || meta.itemStatus is CIStatus.SndRcvd) {
Icon(painterResource(icon), null, Modifier.height(17.dp), tint = statusColor)
} else {
StatusIconText(painterResource(icon), statusColor)
}
Spacer(Modifier.width(4.dp))
} else if (!meta.disappearing) {
StatusIconText(painterResource(MR.images.ic_circle_filled), Color.Transparent)

View File

@@ -12,7 +12,11 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
@@ -29,6 +33,7 @@ import chat.simplex.app.views.isValidDisplayName
import chat.simplex.app.views.localauth.SetAppPasscodeView
import chat.simplex.app.views.onboarding.ReadableText
import chat.simplex.res.MR
import kotlinx.coroutines.runBlocking
enum class LAMode {
SYSTEM,
@@ -79,6 +84,52 @@ fun PrivacySettingsView(
if (chatModel.simplexLinkMode.value == SimplexLinkMode.BROWSER) {
SectionTextFooter(stringResource(MR.strings.simplex_link_mode_browser_warning))
}
SectionDividerSpaced()
val currentUser = chatModel.currentUser.value
if (currentUser != null) {
fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) {
withApi {
val mrs = UserMsgReceiptSettings(enable, clearOverrides)
chatModel.controller.apiSetUserContactReceipts(currentUser.userId, mrs)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
chatModel.currentUser.value = currentUser.copy(sendRcptsContacts = enable)
if (clearOverrides) {
// For loop here is to prevent ConcurrentModificationException that happens with forEach
for (i in 0 until chatModel.chats.size) {
val chat = chatModel.chats[i]
if (chat.chatInfo is ChatInfo.Direct) {
var contact = chat.chatInfo.contact
val sendRcpts = contact.chatSettings.sendRcpts
if (sendRcpts != null && sendRcpts != enable) {
contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null))
chatModel.updateContact(contact)
}
}
}
}
}
}
DeliveryReceiptsSection(
currentUser = currentUser,
setOrAskSendReceiptsContacts = { enable ->
val contactReceiptsOverrides = chatModel.chats.fold(0) { count, chat ->
if (chat.chatInfo is ChatInfo.Direct) {
val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts
count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1)
} else {
count
}
}
if (contactReceiptsOverrides == 0) {
setSendReceiptsContacts(enable, clearOverrides = false)
} else {
showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts)
}
}
)
}
SectionBottomSpacer()
}
}
@@ -104,6 +155,70 @@ private fun SimpleXLinkOptions(simplexLinkModeState: State<SimplexLinkMode>, onS
)
}
@Composable
private fun DeliveryReceiptsSection(
currentUser: User,
setOrAskSendReceiptsContacts: (Boolean) -> Unit,
) {
SectionView(stringResource(MR.strings.settings_section_title_delivery_receipts)) {
SettingsActionItemWithContent(painterResource(MR.images.ic_person), stringResource(MR.strings.receipts_section_contacts)) {
DefaultSwitch(
checked = currentUser.sendRcptsContacts ?: false,
onCheckedChange = { enable ->
setOrAskSendReceiptsContacts(enable)
}
)
}
}
SectionTextFooter(
remember(currentUser.displayName) {
buildAnnotatedString {
append(generalGetString(MR.strings.receipts_section_description) + " ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(currentUser.displayName)
}
append(".\n")
append(generalGetString(MR.strings.receipts_section_description_1))
}
}
)
}
private fun showUserContactsReceiptsAlert(
enable: Boolean,
contactReceiptsOverrides: Int,
setSendReceiptsContacts: (Boolean, Boolean) -> Unit
) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(if (enable) MR.strings.receipts_contacts_title_enable else MR.strings.receipts_contacts_title_disable),
text = AnnotatedString(String.format(generalGetString(if (enable) MR.strings.receipts_contacts_override_disabled else MR.strings.receipts_contacts_override_enabled), contactReceiptsOverrides)),
buttons = {
Column {
SectionItemView({
AlertManager.shared.hideAlert()
setSendReceiptsContacts(enable, false)
}) {
val t = stringResource(if (enable) MR.strings.receipts_contacts_enable_keep_overrides else MR.strings.receipts_contacts_disable_keep_overrides)
Text(t, Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
SectionItemView({
AlertManager.shared.hideAlert()
setSendReceiptsContacts(enable, true)
}
) {
val t = stringResource(if (enable) MR.strings.receipts_contacts_enable_for_all else MR.strings.receipts_contacts_disable_for_all)
Text(t, Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
}
SectionItemView({
AlertManager.shared.hideAlert()
}) {
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.onBackground)
}
}
}
)
}
private val laDelays = listOf(10, 30, 60, 180, 0)
@Composable

View File

@@ -0,0 +1,123 @@
package chat.simplex.app.views.usersettings
import SectionBottomSpacer
import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.style.TextAlign
import chat.simplex.app.TAG
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR
@Composable
fun SetDeliveryReceiptsView(m: ChatModel) {
SetDeliveryReceiptsLayout(
enableReceipts = {
val currentUser = m.currentUser.value
if (currentUser != null) {
withApi {
try {
m.controller.apiSetAllContactReceipts(enable = true)
m.currentUser.value = currentUser.copy(sendRcptsContacts = true)
m.setDeliveryReceipts.value = false
m.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
try {
val users = m.controller.listUsers()
m.users.clear()
m.users.addAll(users)
} catch (e: Exception) {
Log.e(TAG, "listUsers error: ${e.stackTraceToString()}")
}
} catch (e: Exception) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.error_enabling_delivery_receipts),
text = e.stackTraceToString()
)
Log.e(TAG, "${generalGetString(MR.strings.error_enabling_delivery_receipts)}: ${e.stackTraceToString()}")
m.setDeliveryReceipts.value = false
}
}
}
},
skip = {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.delivery_receipts_are_disabled),
text = generalGetString(MR.strings.you_can_enable_delivery_receipts_later_alert),
confirmText = generalGetString(MR.strings.ok),
dismissText = generalGetString(MR.strings.dont_show_again),
onConfirm = {
m.setDeliveryReceipts.value = false
},
onDismiss = {
m.setDeliveryReceipts.value = false
m.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
}
)
},
userCount = m.users.size
)
}
@Composable
private fun SetDeliveryReceiptsLayout(
enableReceipts: () -> Unit,
skip: () -> Unit,
userCount: Int,
) {
Column(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(top = DEFAULT_PADDING),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AppBarTitle(stringResource(MR.strings.delivery_receipts_title))
Spacer(Modifier.weight(1f))
EnableReceiptsButton(enableReceipts)
if (userCount > 1) {
TextBelowButton(stringResource(MR.strings.sending_delivery_receipts_will_be_enabled_all_profiles))
} else {
TextBelowButton(stringResource(MR.strings.sending_delivery_receipts_will_be_enabled))
}
Spacer(Modifier.weight(1f))
SkipButton(skip)
SectionBottomSpacer()
}
}
@Composable
private fun EnableReceiptsButton(onClick: () -> Unit) {
TextButton(onClick) {
Text(stringResource(MR.strings.enable_receipts_all), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary)
}
}
@Composable
private fun SkipButton(onClick: () -> Unit) {
SimpleButtonIconEnded(stringResource(MR.strings.dont_enable_receipts), painterResource(MR.images.ic_chevron_right), click = onClick)
TextBelowButton(stringResource(MR.strings.you_can_enable_delivery_receipts_later))
}
@Composable
private fun TextBelowButton(text: String) {
Text(
text,
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING * 3),
style = MaterialTheme.typography.subtitle1,
textAlign = TextAlign.Center,
)
}

View File

@@ -864,6 +864,17 @@
<string name="empty_chat_profile_is_created">An empty chat profile with the provided name is created, and the app opens as usual.</string>
<string name="if_you_enter_passcode_data_removed">If you enter this passcode when opening the app, all app data will be irreversibly removed!</string>
<string name="set_passcode">Set passcode</string>
<string name="receipts_section_description">These settings are for your current profile</string>
<string name="receipts_section_description_1">They can be overridden in contact settings</string>
<string name="receipts_section_contacts">Contacts</string>
<string name="receipts_contacts_title_enable">Enable receipts?</string>
<string name="receipts_contacts_title_disable">Disable receipts?</string>
<string name="receipts_contacts_override_enabled">Sending receipts is enabled for %d contacts</string>
<string name="receipts_contacts_override_disabled">Sending receipts is disabled for %d contacts</string>
<string name="receipts_contacts_enable_keep_overrides">Enable (keep overrides)</string>
<string name="receipts_contacts_disable_keep_overrides">Disable (keep overrides)</string>
<string name="receipts_contacts_enable_for_all">Enable for all</string>
<string name="receipts_contacts_disable_for_all">Disable for all</string>
<!-- Settings sections -->
<string name="settings_section_title_you">YOU</string>
@@ -873,6 +884,7 @@
<string name="settings_section_title_app">APP</string>
<string name="settings_section_title_device">DEVICE</string>
<string name="settings_section_title_chats">CHATS</string>
<string name="settings_section_title_delivery_receipts">SEND DELIVERY RECEIPTS TO</string>
<string name="settings_restart_app">Restart</string>
<string name="settings_shutdown">Shutdown</string>
<string name="settings_developer_tools">Developer tools</string>
@@ -1144,6 +1156,7 @@
<string name="address_section_title">Address</string>
<string name="share_address">Share address</string>
<string name="you_can_share_this_address_with_your_contacts">You can share this address with your contacts to let them connect with %s.</string>
<string name="send_receipts">Send receipts</string>
<!-- Chat / Chat item info -->
<string name="section_title_for_console">FOR CONSOLE</string>
@@ -1487,7 +1500,7 @@
<string name="v5_2_disappear_one_message">Make one message disappear</string>
<string name="v5_2_disappear_one_message_descr">Even when disabled in the conversation.</string>
<string name="v5_2_more_things">A few more things</string>
<string name="v5_2_more_things_descr">- more stabile message delivery.\n- a bit better groups.\n- and more!</string>
<string name="v5_2_more_things_descr">- more stable message delivery.\n- a bit better groups.\n- and more!</string>
<!-- CustomTimePicker -->
<string name="custom_time_unit_seconds">seconds</string>
@@ -1498,4 +1511,15 @@
<string name="custom_time_unit_months">months</string>
<string name="custom_time_picker_select">Select</string>
<string name="custom_time_picker_custom">custom</string>
<!-- SetDeliveryReceiptsView.kt -->
<string name="delivery_receipts_title">Delivery receipts!</string>
<string name="enable_receipts_all">Enable</string>
<string name="sending_delivery_receipts_will_be_enabled_all_profiles">Sending delivery receipts will be enabled for all contacts in all visible chat profiles.</string>
<string name="sending_delivery_receipts_will_be_enabled">Sending delivery receipts will be enabled for all contacts.</string>
<string name="dont_enable_receipts">Don\'t enable</string>
<string name="you_can_enable_delivery_receipts_later">You can enable later via Settings</string>
<string name="delivery_receipts_are_disabled">Delivery receipts are disabled!</string>
<string name="you_can_enable_delivery_receipts_later_alert">You can enable them later via app Privacy &amp; Security settings.</string>
<string name="error_enabling_delivery_receipts">Error enabling delivery receipts!</string>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 48 48" xml:space="preserve">
<desc>Created with Fabric.js 5.3.0</desc>
<defs>
</defs>
<g transform="matrix(0.05 0 0 0.05 16.9226377203 24)" id="cyIAcaUiI3wHfSH5bhc9b" >
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-0.0000029117, 0)" d="M -121.11468 211.5 L -302.11468 31 C -307.78135000000003 25.33333 -310.44801 18.5 -310.11468 10.5 C -309.78135000000003 2.5 -306.78135000000003 -4.5 -301.11468 -10.5 C -295.44801 -16.16667 -288.61468 -19 -280.61468 -19 C -272.61468 -19 -265.61468 -16.16667 -259.61468 -10.5 L -99.51660000000001 151.09808 C -99.51660000000001 151.09808 -59.52603000000001 109.45121 -38.76350000000001 89.33764000000001 C -17.203620000000008 111.33126000000001 -17.21119000000001 110.30775 1.5472499999999911 129.00625000000002 C -20.44541000000001 150.10187000000002 -59.61468000000001 190.50000000000003 -59.61468000000001 190.50000000000003 L -80.61468 212.00000000000003 C -86.61468 217.66667000000004 -93.44801000000001 220.50000000000003 -101.11468 220.50000000000003 C -108.78135 220.50000000000003 -115.44801000000001 217.50000000000003 -121.11468 211.50000000000003 z M 121.98107 7.30617 L 81.94011 -31.93575 L 260.38532 -212 C 266.38532 -217.66667 273.38532 -220.5 281.38532 -220.5 C 289.38532 -220.5 296.21864999999997 -217.5 301.88532 -211.5 C 307.55199 -205.83333 310.30199 -199 310.13532 -191 C 309.96864999999997 -183 307.21864999999997 -176.16667 301.88532 -170.5 z" stroke-linecap="round" />
</g>
<g transform="matrix(0.05 0 0 0.05 31.1326746345 23.9932210404)" id="jtaF79RV_gXXrwaj4xd1d" >
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-0.0000013443, 0.0000015673)" d="M -121.23611 211.37857 L -302.23611 30.878569999999996 C -307.90278 25.211899999999996 -310.56944 18.378569999999996 -310.23611 10.378569999999996 C -309.90278 2.3785699999999963 -306.73611 -4.621430000000004 -300.73611 -10.621430000000004 C -295.06944 -16.288100000000004 -288.31944 -19.121430000000004 -280.48611 -19.121430000000004 C -272.65278 -19.121430000000004 -265.73611 -16.288100000000004 -259.73611 -10.621430000000004 L -101.23611 149.37857 L 260.26389 -212.12143 C 265.93056 -217.45476 272.76389 -220.20476 280.76389 -220.37143 C 288.76389 -220.53810000000001 295.59722 -217.78810000000001 301.26389 -212.12143 C 307.26389 -206.45476 310.26389 -199.62143 310.26389 -191.62143 C 310.26389 -183.62143 307.43056 -176.78810000000001 301.76389 -171.12143 L -80.73611 211.37857 C -86.73611 217.37857 -93.56944 220.37857 -101.23611 220.37857 C -108.90277999999999 220.37857 -115.56944 217.37857 -121.23611 211.37857 z" stroke-linecap="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB