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:
committed by
GitHub
parent
1bd8f66730
commit
d3b5bbe566
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 & Security settings.</string>
|
||||
<string name="error_enabling_delivery_receipts">Error enabling delivery receipts!</string>
|
||||
</resources>
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user