From 07ef6e4090dad04218a9e8374db1a8175a9bcd89 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Sat, 3 Dec 2022 15:40:31 +0400 Subject: [PATCH] ios: marked deleted chat items, full deletion preference; android: types (#1473) * ios: marked deleted chat items; full deletion preference * text_, menu, backend * android types * more android types * fix * refactor ios * restore previews * box * refactor menu * revert unnecessary content.text changes * Update apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> * revert layered framed items * clever framed view * improve look * restore previews * restore previews * refactor * refactoring, almost looks good * look * add previews * more previews * remove preview of legacy item * ChatItemDeleted * flip if * remove text_ * refactor * abstract pref property * move marked deleted * revert pref change * undo menu * fix - change to constants * undo pref logic Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/model/ChatModel.kt | 63 +++-- .../java/chat/simplex/app/model/NtfManager.kt | 10 +- .../java/chat/simplex/app/model/SimpleXAPI.kt | 37 +-- .../chat/simplex/app/views/chat/ChatView.kt | 11 +- .../simplex/app/views/chat/ComposeView.kt | 3 +- .../app/views/chat/ComposeVoiceView.kt | 3 +- .../app/views/chat/ContactPreferences.kt | 10 +- .../app/views/chat/group/GroupPreferences.kt | 10 +- .../app/views/chat/item/CICallItemView.kt | 3 +- .../app/views/chat/item/CIVoiceView.kt | 2 +- .../app/views/chat/item/FramedItemView.kt | 2 + .../app/views/chatlist/ChatPreviewView.kt | 4 +- .../chat/simplex/app/views/helpers/Util.kt | 2 - .../app/views/usersettings/Preferences.kt | 10 +- .../app/src/main/res/values-de/strings.xml | 2 + .../app/src/main/res/values-ru/strings.xml | 2 + .../app/src/main/res/values/strings.xml | 2 + apps/ios/Shared/Model/SimpleXAPI.swift | 15 +- .../Views/Chat/ChatItem/CIFileView.swift | 20 +- .../Views/Chat/ChatItem/CIMetaView.swift | 2 +- .../Views/Chat/ChatItem/CIVoiceView.swift | 8 +- .../Chat/ChatItem/FramedCIVoiceView.swift | 10 +- .../Views/Chat/ChatItem/FramedItemView.swift | 236 ++++++++++------ .../Chat/ChatItem/MarkedDeletedItemView.swift | 44 +++ apps/ios/Shared/Views/Chat/ChatItemView.swift | 113 ++++++-- apps/ios/Shared/Views/Chat/ChatView.swift | 254 +++++++++++------- .../Chat/ComposeMessage/ContextItemView.swift | 6 +- .../Views/Chat/ContactPreferencesView.swift | 2 +- .../Chat/Group/GroupPreferencesView.swift | 2 +- .../Views/ChatList/ChatPreviewView.swift | 8 +- .../Views/UserSettings/PreferencesView.swift | 2 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 44 +-- apps/ios/SimpleXChat/APITypes.swift | 4 +- apps/ios/SimpleXChat/ChatTypes.swift | 20 +- src/Simplex/Chat.hs | 24 +- src/Simplex/Chat/Messages.hs | 2 +- src/Simplex/Chat/Store.hs | 4 +- 37 files changed, 643 insertions(+), 353 deletions(-) create mode 100644 apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 9a73ec4f52..68cad140b0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -392,6 +392,9 @@ interface SomeChat { val ready: Boolean val sendMsgEnabled: Boolean val ntfsEnabled: Boolean + val incognito: Boolean + val voiceMessageAllowed: Boolean + val fullDeletionAllowed: Boolean val createdAt: Instant val updatedAt: Instant } @@ -442,7 +445,6 @@ data class Chat ( @Serializable sealed class ChatInfo: SomeChat, NamedChat { - abstract val incognito: Boolean @Serializable @SerialName("direct") data class Direct(val contact: Contact): ChatInfo() { @@ -452,8 +454,10 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contact.apiId override val ready get() = contact.ready override val sendMsgEnabled get() = contact.sendMsgEnabled - override val ntfsEnabled get() = contact.chatSettings.enableNtfs - override val incognito get() = contact.contactConnIncognito + override val ntfsEnabled get() = contact.ntfsEnabled + override val incognito get() = contact.incognito + override val voiceMessageAllowed get() = contact.voiceMessageAllowed + override val fullDeletionAllowed get() = contact.fullDeletionAllowed override val createdAt get() = contact.createdAt override val updatedAt get() = contact.updatedAt override val displayName get() = contact.displayName @@ -474,8 +478,10 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = groupInfo.apiId override val ready get() = groupInfo.ready override val sendMsgEnabled get() = groupInfo.sendMsgEnabled - override val ntfsEnabled get() = groupInfo.chatSettings.enableNtfs - override val incognito get() = groupInfo.membership.memberIncognito + override val ntfsEnabled get() = groupInfo.ntfsEnabled + override val incognito get() = groupInfo.incognito + override val voiceMessageAllowed get() = groupInfo.voiceMessageAllowed + override val fullDeletionAllowed get() = groupInfo.fullDeletionAllowed override val createdAt get() = groupInfo.createdAt override val updatedAt get() = groupInfo.updatedAt override val displayName get() = groupInfo.displayName @@ -496,8 +502,10 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contactRequest.apiId override val ready get() = contactRequest.ready override val sendMsgEnabled get() = contactRequest.sendMsgEnabled - override val ntfsEnabled get() = false - override val incognito get() = false + override val ntfsEnabled get() = contactRequest.ntfsEnabled + override val incognito get() = contactRequest.incognito + override val voiceMessageAllowed get() = contactRequest.voiceMessageAllowed + override val fullDeletionAllowed get() = contactRequest.fullDeletionAllowed override val createdAt get() = contactRequest.createdAt override val updatedAt get() = contactRequest.updatedAt override val displayName get() = contactRequest.displayName @@ -518,8 +526,10 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contactConnection.apiId override val ready get() = contactConnection.ready override val sendMsgEnabled get() = contactConnection.sendMsgEnabled - override val ntfsEnabled get() = false + override val ntfsEnabled get() = contactConnection.incognito override val incognito get() = contactConnection.incognito + override val voiceMessageAllowed get() = contactConnection.voiceMessageAllowed + override val fullDeletionAllowed get() = contactConnection.fullDeletionAllowed override val createdAt get() = contactConnection.createdAt override val updatedAt get() = contactConnection.updatedAt override val displayName get() = contactConnection.displayName @@ -553,6 +563,9 @@ data class Contact( override val ready get() = activeConn.connStatus == ConnStatus.Ready override val sendMsgEnabled get() = true override val ntfsEnabled get() = chatSettings.enableNtfs + override val incognito get() = contactConnIncognito + override val voiceMessageAllowed get() = mergedPreferences.voice.enabled.forUser + override val fullDeletionAllowed get() = mergedPreferences.fullDelete.enabled.forUser override val displayName get() = localAlias.ifEmpty { profile.displayName } override val fullName get() = profile.fullName override val image get() = profile.image @@ -675,6 +688,9 @@ data class GroupInfo ( override val ready get() = membership.memberActive override val sendMsgEnabled get() = membership.memberActive override val ntfsEnabled get() = chatSettings.enableNtfs + override val incognito get() = membership.memberIncognito + override val voiceMessageAllowed get() = fullGroupPreferences.voice.on + override val fullDeletionAllowed get() = fullGroupPreferences.fullDelete.on override val displayName get() = groupProfile.displayName override val fullName get() = groupProfile.fullName override val image get() = groupProfile.image @@ -918,6 +934,9 @@ class UserContactRequest ( override val ready get() = true override val sendMsgEnabled get() = false override val ntfsEnabled get() = false + override val incognito get() = false + override val voiceMessageAllowed get() = false + override val fullDeletionAllowed get() = false override val displayName get() = profile.displayName override val fullName get() = profile.fullName override val image get() = profile.image @@ -953,6 +972,9 @@ class PendingContactConnection( override val ready get() = false override val sendMsgEnabled get() = false override val ntfsEnabled get() = false + override val incognito get() = customUserProfileId != null + override val voiceMessageAllowed get() = false + override val fullDeletionAllowed get() = false override val localDisplayName get() = String.format(generalGetString(R.string.connection_local_display_name), pccConnId) override val displayName: String get() { if (localAlias.isNotEmpty()) return localAlias @@ -972,8 +994,6 @@ class PendingContactConnection( val initiated get() = (pccConnStatus.initiated ?: false) && !viaContactUri - val incognito = customUserProfileId != null - val description: String get() { val initiated = pccConnStatus.initiated return if (initiated == null) "" else generalGetString( @@ -1043,14 +1063,14 @@ data class ChatItem ( val id: Long get() = meta.itemId val timestampText: String get() = meta.timestampText - val text: String get() = - when { - content.text == "" && file != null && content.msgContent is MsgContent.MCVoice -> { - (content.msgContent as MsgContent.MCVoice).toTextWithDuration(false) - } + val text: String get() { + val mc = content.msgContent + return when { + content.text == "" && file != null && mc is MsgContent.MCVoice -> String.format(generalGetString(R.string.voice_message_with_duration), durationText(mc.duration)) content.text == "" && file != null -> file.fileName else -> content.text } + } val isRcvNew: Boolean get() = meta.itemStatus is CIStatus.RcvNew @@ -1318,8 +1338,8 @@ class CIQuote ( val formattedText: List? = null ): ItemContent { override val text: String by lazy { - if (content is MsgContent.MCVoice && content.text.isEmpty()) - content.toTextWithDuration(true) + if (content.text == "" && content is MsgContent.MCVoice) + durationText(content.duration) else content.text } @@ -1406,11 +1426,6 @@ sealed class MsgContent { } } -fun MsgContent.MCVoice.toTextWithDuration(short: Boolean): String { - val time = durationToString(duration) - return if (short) time else generalGetString(R.string.voice_message) + " ($time)" -} - @Serializable class CIGroupInvitation ( val groupId: Long, @@ -1644,11 +1659,13 @@ enum class CICallStatus { Accepted -> generalGetString(R.string.callstatus_accepted) Negotiated -> generalGetString(R.string.callstatus_connecting) Progress -> generalGetString(R.string.callstatus_in_progress) - Ended -> String.format(generalGetString(R.string.callstatus_ended), durationToString(sec)) + Ended -> String.format(generalGetString(R.string.callstatus_ended), durationText(sec)) Error -> generalGetString(R.string.callstatus_error) } } +fun durationText(sec: Int): String = "%02d:%02d".format(sec / 60, sec % 60) + @Serializable sealed class MsgErrorType() { @Serializable @SerialName("msgSkipped") class MsgSkipped(val fromMsgId: Long, val toMsgId: Long): MsgErrorType() diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt index f1f7ae8179..b90b6cc302 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/NtfManager.kt @@ -217,18 +217,14 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference private fun hideSecrets(cItem: ChatItem) : String { val md = cItem.formattedText - return if (md == null) { - if (cItem.content.text != "") { - cItem.content.text - } else { - if (cItem.content.msgContent is MsgContent.MCVoice) generalGetString(R.string.voice_message) else cItem.file?.fileName ?: "" - } - } else { + return if (md != null) { var res = "" for (ft in md) { res += if (ft.format is Format.Secret) "..." else ft.text } res + } else { + cItem.text } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index b82fe0a589..cd8725e174 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -435,9 +435,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a return null } - suspend fun apiDeleteChatItem(type: ChatType, id: Long, itemId: Long, mode: CIDeleteMode): AChatItem? { + suspend fun apiDeleteChatItem(type: ChatType, id: Long, itemId: Long, mode: CIDeleteMode): CR.ChatItemDeleted? { val r = sendCmd(CC.ApiDeleteChatItem(type, id, itemId, mode)) - if (r is CR.ChatItemDeleted) return r.toChatItem + if (r is CR.ChatItemDeleted) return r Log.e(TAG, "apiDeleteChatItem bad response: ${r.responseType} ${r.details}") return null } @@ -1069,14 +1069,11 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a is CR.ChatItemUpdated -> chatItemSimpleUpdate(r.chatItem) is CR.ChatItemDeleted -> { - val cInfo = r.toChatItem.chatInfo - val cItem = r.toChatItem.chatItem - if (cItem.meta.itemDeleted) { - chatModel.removeChatItem(cInfo, cItem) + AudioPlayer.stop(r.deletedChatItem.chatItem) + if (r.toChatItem == null) { + chatModel.removeChatItem(r.deletedChatItem.chatInfo, r.deletedChatItem.chatItem) } else { - // currently only broadcast deletion of rcv message can be received, and only this case should happen - AudioPlayer.stop(cItem) - chatModel.upsertChatItem(cInfo, cItem) + chatModel.upsertChatItem(r.toChatItem.chatInfo, r.toChatItem.chatItem) } } is CR.ReceivedGroupInvitation -> { @@ -1959,7 +1956,6 @@ data class FullChatPreferences( val fullDelete: ChatPreference, val voice: ChatPreference, ) { - fun toPreferences(): ChatPreferences = ChatPreferences(fullDelete = fullDelete, voice = voice) companion object { @@ -1972,7 +1968,6 @@ data class ChatPreferences( val fullDelete: ChatPreference? = null, val voice: ChatPreference? = null, ) { - companion object { val sampleData = ChatPreferences(fullDelete = ChatPreference(allow = FeatureAllowed.NO), voice = ChatPreference(allow = FeatureAllowed.YES)) } @@ -2041,8 +2036,16 @@ data class FeatureEnabled( @Serializable sealed class ContactUserPref { - @Serializable @SerialName("contact") data class Contact(val preference: ChatPreference): ContactUserPref() // contact override is set - @Serializable @SerialName("user") data class User(val preference: ChatPreference): ContactUserPref() // global user default is used + abstract val pref: ChatPreference + + // contact override is set + @Serializable @SerialName("contact") data class Contact(val preference: ChatPreference): ContactUserPref() { + override val pref get() = preference + } + // global user default is used + @Serializable @SerialName("user") data class User(val preference: ChatPreference): ContactUserPref() { + override val pref get() = preference + } } interface Feature { @@ -2277,7 +2280,9 @@ data class GroupPreferences( @Serializable data class GroupPreference( val enable: GroupFeatureEnabled -) +) { + val on: Boolean get() = enable == GroupFeatureEnabled.ON +} @Serializable enum class GroupFeatureEnabled { @@ -2372,7 +2377,7 @@ sealed class CR { @Serializable @SerialName("newChatItem") class NewChatItem(val chatItem: AChatItem): CR() @Serializable @SerialName("chatItemStatusUpdated") class ChatItemStatusUpdated(val chatItem: AChatItem): CR() @Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val chatItem: AChatItem): CR() - @Serializable @SerialName("chatItemDeleted") class ChatItemDeleted(val deletedChatItem: AChatItem, val toChatItem: AChatItem): CR() + @Serializable @SerialName("chatItemDeleted") class ChatItemDeleted(val deletedChatItem: AChatItem, val toChatItem: AChatItem? = null, val byUser: Boolean): CR() @Serializable @SerialName("contactsList") class ContactsList(val contacts: List): CR() // group events @Serializable @SerialName("groupCreated") class GroupCreated(val groupInfo: GroupInfo): CR() @@ -2566,7 +2571,7 @@ sealed class CR { is NewChatItem -> json.encodeToString(chatItem) is ChatItemStatusUpdated -> json.encodeToString(chatItem) is ChatItemUpdated -> json.encodeToString(chatItem) - is ChatItemDeleted -> "deletedChatItem:\n${json.encodeToString(deletedChatItem)}\ntoChatItem:\n${json.encodeToString(toChatItem)}" + is ChatItemDeleted -> "deletedChatItem:\n${json.encodeToString(deletedChatItem)}\ntoChatItem:\n${json.encodeToString(toChatItem)}\nbyUser: $byUser" is ContactsList -> json.encodeToString(contacts) is GroupCreated -> json.encodeToString(groupInfo) is SentGroupInvitation -> "groupInfo: $groupInfo\ncontact: $contact\nmember: $member" diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 45433b49bd..a1299f9e7b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -166,13 +166,20 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { deleteMessage = { itemId, mode -> withApi { val cInfo = chat.chatInfo - val toItem = chatModel.controller.apiDeleteChatItem( + val r = chatModel.controller.apiDeleteChatItem( type = cInfo.chatType, id = cInfo.apiId, itemId = itemId, mode = mode ) - if (toItem != null) chatModel.removeChatItem(cInfo, toItem.chatItem) + if (r != null) { + val toChatItem = r.toChatItem + if (toChatItem == null) { + chatModel.removeChatItem(cInfo, r.deletedChatItem.chatItem) + } else { + chatModel.upsertChatItem(cInfo, toChatItem.chatItem) + } + } } }, receiveFile = { fileId -> diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt index 47a78048f1..f859dd68e3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt @@ -150,7 +150,7 @@ fun ComposeView( val textStyle = remember { mutableStateOf(smallFont) } // attachments val chosenContent = rememberSaveable { mutableStateOf>(emptyList()) } - val audioSaver = Saver?>, Pair> ( + val audioSaver = Saver?>, Pair>( save = { it.value.let { if (it == null) null else it.first.toString() to it.second } }, restore = { mutableStateOf(Uri.parse(it.first) to it.second) } ) @@ -466,6 +466,7 @@ fun ComposeView( } } } + fun showDisabledVoiceAlert() { AlertManager.shared.showAlertMsg( title = generalGetString(R.string.voice_messages_prohibited), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeVoiceView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeVoiceView.kt index ae8c9a9022..c434b8eafb 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeVoiceView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeVoiceView.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.R +import chat.simplex.app.model.durationText import chat.simplex.app.ui.theme.* import chat.simplex.app.views.chat.item.SentColorLight import chat.simplex.app.views.helpers.* @@ -94,7 +95,7 @@ fun ComposeVoiceView( } } Text( - durationToString(numberInText.value), + durationText(numberInText.value), fontSize = 18.sp, color = HighOrLowlight, ) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt index 65ed8861e3..8fb3b41feb 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt @@ -72,11 +72,11 @@ private fun ContactPreferencesLayout( horizontalAlignment = Alignment.Start, ) { AppBarTitle(stringResource(R.string.contact_preferences)) -// val allowFullDeletion: MutableState = remember(featuresAllowed) { mutableStateOf(featuresAllowed.fullDelete) } -// FeatureSection(Feature.FullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, allowFullDeletion) { -// applyPrefs(featuresAllowed.copy(fullDelete = it)) -// } -// SectionSpacer() + val allowFullDeletion: MutableState = remember(featuresAllowed) { mutableStateOf(featuresAllowed.fullDelete) } + FeatureSection(ChatFeature.FullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, allowFullDeletion) { + applyPrefs(featuresAllowed.copy(fullDelete = it)) + } + SectionSpacer() val allowVoice: MutableState = remember(featuresAllowed) { mutableStateOf(featuresAllowed.voice) } FeatureSection(ChatFeature.Voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, allowVoice) { applyPrefs(featuresAllowed.copy(voice = it)) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt index fb2ffe867a..68a0db0211 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt @@ -66,11 +66,11 @@ private fun GroupPreferencesLayout( FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, groupInfo) { applyPrefs(preferences.copy(directMessages = GroupPreference(enable = it))) } -// val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.enable) } -// FeatureSection(Feature.FullDelete, allowFullDeletion, groupInfo) { -// applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = it))) -// } -// SectionSpacer() + val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.enable) } + FeatureSection(GroupFeature.FullDelete, allowFullDeletion, groupInfo) { + applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = it))) + } + SectionSpacer() val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.enable) } FeatureSection(GroupFeature.Voice, allowVoice, groupInfo) { applyPrefs(preferences.copy(voice = GroupPreference(enable = it))) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt index b9f9a21c97..5ab80774c6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.unit.sp import chat.simplex.app.R import chat.simplex.app.model.* import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.durationToString @Composable fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, duration: Int, acceptCall: (Contact) -> Unit) { @@ -39,7 +38,7 @@ fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, durat CICallStatus.Progress -> Icon(Icons.Filled.PhoneInTalk, stringResource(R.string.icon_descr_call_progress), tint = SimplexGreen) CICallStatus.Ended -> Row { Icon(Icons.Outlined.CallEnd, stringResource(R.string.icon_descr_call_ended), tint = HighOrLowlight, modifier = Modifier.padding(end = 4.dp)) - Text(durationToString(duration), color = HighOrLowlight) + Text(durationText(duration), color = HighOrLowlight) } CICallStatus.Error -> {} } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt index 7b7bc849d6..08f297b097 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt @@ -59,7 +59,7 @@ fun CIVoiceView( audioPlaying.value || progress.value != 0 -> progress.value else -> duration.value } - durationToString(time / 1000) + durationText(time / 1000) } } VoiceLayout(file, ci, metaColor, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, play, pause, longClick) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt index 06b04644e1..1851bfe1b9 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt @@ -115,6 +115,7 @@ fun FramedItemView( } } + // TODO MCVoice shouldn't affect it, see https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901 val transparentBackground = (ci.content.msgContent is MsgContent.MCImage || ci.content.msgContent is MsgContent.MCVoice) && ci.content.text.isEmpty() && ci.quotedItem == null Box(Modifier @@ -175,6 +176,7 @@ fun FramedItemView( } } } + // TODO CIMetaView shouldn't be conditional, see https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901 if (ci.content.msgContent !is MsgContent.MCVoice || ci.content.text.isNotEmpty() || ci.quotedItem != null) { Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) { CIMetaView(ci, metaColor) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index 860861a5fd..11fb038766 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -83,8 +83,8 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD val ci = chat.chatItems.lastOrNull() if (ci != null) { MarkdownText( - ci.text, - ci.formattedText, + if (!ci.meta.itemDeleted) ci.text else generalGetString(R.string.marked_deleted_description), + if (!ci.meta.itemDeleted) ci.formattedText else null, sender = if (cInfo is ChatInfo.Group && !ci.chatDir.sent) ci.memberDisplayName else null, linkMode = linkMode, senderBold = true, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt index c1a4cc4286..2490a903bb 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -447,8 +447,6 @@ fun directoryFileCountAndSize(dir: String): Pair { // count, size in return fileCount to bytes } -fun durationToString(sec: Int): String = "%02d:%02d".format(sec / 60, sec % 60) - fun Color.darker(factor: Float = 0.1f): Color = Color(max(red * (1 - factor), 0f), max(green * (1 - factor), 0f), max(blue * (1 - factor), 0f), alpha) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt index e5ae5bcffd..33494d9160 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt @@ -61,11 +61,11 @@ private fun PreferencesLayout( horizontalAlignment = Alignment.Start, ) { AppBarTitle(stringResource(R.string.your_preferences)) -// val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.allow) } -// FeatureSection(Feature.FullDelete, allowFullDeletion) { -// applyPrefs(preferences.copy(fullDelete = ChatPreference(allow = it))) -// } -// SectionSpacer() + val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.allow) } + FeatureSection(ChatFeature.FullDelete, allowFullDeletion) { + applyPrefs(preferences.copy(fullDelete = ChatPreference(allow = it))) + } + SectionSpacer() val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.allow) } FeatureSection(ChatFeature.Voice, allowVoice) { applyPrefs(preferences.copy(voice = ChatPreference(allow = it))) diff --git a/apps/android/app/src/main/res/values-de/strings.xml b/apps/android/app/src/main/res/values-de/strings.xml index 9220755abe..45bc316f49 100644 --- a/apps/android/app/src/main/res/values-de/strings.xml +++ b/apps/android/app/src/main/res/values-de/strings.xml @@ -21,6 +21,7 @@ Gelöscht + ***marked deleted Das Senden von Dateien wird noch nicht unterstützt Der Empfang von Dateien wird noch nicht unterstützt Meine Daten @@ -230,6 +231,7 @@ Sprachnachricht + Sprachnachricht (%1$s) Sprachnachricht… diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index fc92183f67..895e3e2201 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -21,6 +21,7 @@ удалено + помечено как удаленное отправка файлов не поддерживается получение файлов не поддерживается вы @@ -230,6 +231,7 @@ Голосовое сообщение + Голосовое сообщение (%1$s) Голосовое сообщение… diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 6cb78aa3b4..a892af2547 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -21,6 +21,7 @@ deleted + marked deleted sending files is not supported yet receiving files is not supported yet you @@ -230,6 +231,7 @@ Voice message + Voice message (%1$s) Voice message… diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index df34e9f86f..19ee4ef6e8 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -261,9 +261,9 @@ func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent throw r } -func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) async throws -> ChatItem { +func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) async throws -> (ChatItem, ChatItem?) { let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemId: itemId, mode: mode), bgDelay: msgDelay) - if case let .chatItemDeleted(_, toChatItem) = r { return toChatItem.chatItem } + if case let .chatItemDeleted(deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) } throw r } @@ -1010,14 +1010,11 @@ func processReceivedMsg(_ res: ChatResponse) async { } case let .chatItemUpdated(aChatItem): chatItemSimpleUpdate(aChatItem) - case let .chatItemDeleted(_, toChatItem): - let cInfo = toChatItem.chatInfo - let cItem = toChatItem.chatItem - if cItem.meta.itemDeleted { - m.removeChatItem(cInfo, cItem) + case let .chatItemDeleted(deletedChatItem, toChatItem, _): + if let toChatItem = toChatItem { + _ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem) } else { - // currently only broadcast deletion of rcv message can be received, and only this case should happen - _ = m.upsertChatItem(cInfo, cItem) + m.removeChatItem(deletedChatItem.chatInfo, deletedChatItem.chatItem) } case let .receivedGroupInvitation(groupInfo, _, _): m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index bf014f8c99..f184568684 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -149,16 +149,16 @@ struct CIFileView_Previews: PreviewProvider { file: nil ) Group { - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentFile) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample()) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation)) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted)) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer)) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled)) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation)) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation)) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation)) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: fileChatItemWtFile) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentFile, revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: fileChatItemWtFile, revealed: Binding.constant(false)) } .previewLayout(.fixed(width: 360, height: 360)) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift index 91d95f3f2a..23224850a0 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -50,7 +50,7 @@ struct CIMetaView: View { struct CIMetaView_Previews: PreviewProvider { static var previews: some View { - return Group { + Group { CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent)) CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, false, true)) CIMetaView(chatItem: ChatItem.getDeletedContentSample()) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift index e7b2b2ad63..13f7bfe3f7 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift @@ -232,13 +232,13 @@ struct CIVoiceView_Previews: PreviewProvider { playbackTime: TimeInterval(20) ) .environmentObject(ChatModel()) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentVoiceMessage) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentVoiceMessage, revealed: Binding.constant(false)) .environmentObject(ChatModel()) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample()) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(), revealed: Binding.constant(false)) .environmentObject(ChatModel()) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer), revealed: Binding.constant(false)) .environmentObject(ChatModel()) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: voiceMessageWtFile) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: voiceMessageWtFile, revealed: Binding.constant(false)) .environmentObject(ChatModel()) } .previewLayout(.fixed(width: 360, height: 360)) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift index d9ca2d2863..803faed175 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift @@ -60,15 +60,15 @@ struct FramedCIVoiceView_Previews: PreviewProvider { file: CIFile.getSample(fileStatus: .sndComplete) ) Group { - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentVoiceMessage) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: sentVoiceMessage, revealed: Binding.constant(false)) .environmentObject(ChatModel()) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there")) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), revealed: Binding.constant(false)) .environmentObject(ChatModel()) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer), revealed: Binding.constant(false)) .environmentObject(ChatModel()) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), revealed: Binding.constant(false)) .environmentObject(ChatModel()) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: voiceMessageWithQuote) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: voiceMessageWithQuote, revealed: Binding.constant(false)) .environmentObject(ChatModel()) } .previewLayout(.fixed(width: 360, height: 360)) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 72331bd716..a101b5fb42 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -26,80 +26,43 @@ struct FramedItemView: View { @State var imgWidth: CGFloat? = nil @State var metaColor = Color.secondary @State var showFullScreenImage = false - + var body: some View { let v = ZStack(alignment: .bottomTrailing) { VStack(alignment: .leading, spacing: 0) { + if chatItem.meta.itemDeleted { + ciDeletedView() + } + if let qi = chatItem.quotedItem { ciQuoteView(qi) .onTapGesture { if let proxy = scrollProxy, let ci = m.reversedChatItems.first(where: { $0.id == qi.itemId }) { - withAnimation { - proxy.scrollTo(ci.viewId, anchor: .bottom) - } + withAnimation { + proxy.scrollTo(ci.viewId, anchor: .bottom) + } } } } - - if chatItem.formattedText == nil && chatItem.file == nil && isShortEmoji(chatItem.content.text) { - VStack { - emojiText(chatItem.content.text) - Text("") - } - .padding(.vertical, 6) - .padding(.horizontal, 12) + + ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, showMember: showMember, msgContentView: framedMsgContentView) + .padding(chatItem.content.msgContent != nil ? 0 : 4) .overlay(DetermineWidth()) - .frame(minWidth: msgWidth, alignment: .center) - .padding(.bottom, 2) - } else { - switch (chatItem.content.msgContent) { - case let .image(text, image): - CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy) - .overlay(DetermineWidth()) - if text == "" { - Color.clear - .frame(width: 0, height: 0) - .preference( - key: MetaColorPreferenceKey.self, - value: .white - ) - } else { - ciMsgContentView (chatItem, showMember) - } - case let .voice(text, duration): - FramedCIVoiceView(chatItem: chatItem, recordingFile: chatItem.file, duration: duration) - .overlay(DetermineWidth()) - if text != "" { - ciMsgContentView (chatItem, showMember) - } - case let .file(text): - ciFileView(chatItem, text) - case let .link(_, preview): - CILinkView(linkPreview: preview) - ciMsgContentView (chatItem, showMember) - case let .unknown(_, text: text): - if chatItem.file == nil { - ciMsgContentView (chatItem, showMember) - } else { - ciFileView(chatItem, text) - } - default: - ciMsgContentView (chatItem, showMember) - } - } } .onPreferenceChange(MetaColorPreferenceKey.self) { metaColor = $0 } - - CIMetaView(chatItem: chatItem, metaColor: metaColor) - .padding(.horizontal, 12) - .padding(.bottom, 6) - .overlay(DetermineWidth()) + + if chatItem.content.msgContent != nil { + CIMetaView(chatItem: chatItem, metaColor: metaColor) + .padding(.horizontal, 12) + .padding(.bottom, 6) + .overlay(DetermineWidth()) + } } - .background(chatItemFrameColor(chatItem, colorScheme)) - .cornerRadius(18) - .onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 } - + .background(chatItemFrameColorMaybeImage(chatItem, colorScheme)) + .cornerRadius(18) + .onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 } + switch chatItem.meta.itemStatus { case .sndErrorAuth: v.onTapGesture { msgDeliveryError("Most likely this contact has deleted the connection with you.") } @@ -108,14 +71,88 @@ struct FramedItemView: View { default: v } } - + + @ViewBuilder private func framedMsgContentView() -> some View { + if chatItem.formattedText == nil && chatItem.file == nil && isShortEmoji(chatItem.content.text) { + VStack { + emojiText(chatItem.content.text) + Text("") + } + .padding(.vertical, 6) + .padding(.horizontal, 12) + .overlay(DetermineWidth()) + .frame(minWidth: msgWidth, alignment: .center) + .padding(.bottom, 2) + } else { + switch (chatItem.content.msgContent) { + case let .image(text, image): + CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy) + .overlay(DetermineWidth()) + if text == "" { + Color.clear + .frame(width: 0, height: 0) + .preference( + key: MetaColorPreferenceKey.self, + value: .white + ) + } else { + ciMsgContentView (chatItem, showMember) + } + case let .voice(text, duration): + FramedCIVoiceView(chatItem: chatItem, recordingFile: chatItem.file, duration: duration) + .overlay(DetermineWidth()) + if text != "" { + ciMsgContentView (chatItem, showMember) + } + case let .file(text): + ciFileView(chatItem, text) + case let .link(_, preview): + CILinkView(linkPreview: preview) + ciMsgContentView (chatItem, showMember) + case let .unknown(_, text: text): + if chatItem.file == nil { + ciMsgContentView (chatItem, showMember) + } else { + ciFileView(chatItem, text) + } + default: + ciMsgContentView (chatItem, showMember) + } + } + } + private func msgDeliveryError(_ err: LocalizedStringKey) { AlertManager.shared.showAlertMsg( title: "Message delivery error", message: err ) } - + + @ViewBuilder private func ciDeletedView() -> some View { + let v = HStack(spacing: 6) { + Image(systemName: "trash") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 14, height: 14) + Text("marked deleted") + .font(.caption) + .italic() + .lineLimit(1) + } + .foregroundColor(.secondary) + .padding(.horizontal, 12) + .padding(.top, 6) + .padding(.bottom, chatItem.quotedItem == nil ? 6 : 0) // TODO think how to regroup + .overlay(DetermineWidth()) + .frame(minWidth: msgWidth, alignment: .leading) + .background(chatItemFrameContextColor(chatItem, colorScheme)) + if let imgWidth = imgWidth, imgWidth < maxWidth { + v.frame(maxWidth: imgWidth, alignment: .leading) + } else { + v + } + } + @ViewBuilder private func ciQuoteView(_ qi: CIQuote) -> some View { let v = ZStack(alignment: .topTrailing) { switch (qi.content) { @@ -144,21 +181,17 @@ struct FramedItemView: View { ciQuotedMsgView(qi) } } - .overlay(DetermineWidth()) - .frame(minWidth: msgWidth, alignment: .leading) - .background( - chatItem.chatDir.sent - ? (colorScheme == .light ? sentQuoteColorLight : sentQuoteColorDark) - : Color(uiColor: .quaternarySystemFill) - ) - + .overlay(DetermineWidth()) + .frame(minWidth: msgWidth, alignment: .leading) + .background(chatItemFrameContextColor(chatItem, colorScheme)) + if let imgWidth = imgWidth, imgWidth < maxWidth { v.frame(maxWidth: imgWidth, alignment: .leading) } else { v } } - + private func ciQuotedMsgView(_ qi: CIQuote) -> some View { MsgContentView( text: qi.text, @@ -170,7 +203,7 @@ struct FramedItemView: View { .padding(.vertical, 6) .padding(.horizontal, 12) } - + private func ciQuoteIconView(_ image: String) -> some View { Image(systemName: image) .resizable() @@ -180,14 +213,14 @@ struct FramedItemView: View { .padding(.top, 6) .padding(.trailing, 6) } - + private func membership() -> GroupMember? { switch chatInfo { case let .group(groupInfo: groupInfo): return groupInfo.membership default: return nil } } - + @ViewBuilder private func ciMsgContentView(_ ci: ChatItem, _ showMember: Bool = false) -> some View { let rtl = isRightToLeft(chatItem.text) let v = MsgContentView( @@ -198,20 +231,20 @@ struct FramedItemView: View { edited: ci.meta.itemEdited, rightToLeft: rtl ) - .multilineTextAlignment(rtl ? .trailing : .leading) - .padding(.vertical, 6) - .padding(.horizontal, 12) - .overlay(DetermineWidth()) - .frame(minWidth: 0, alignment: .leading) - .textSelection(.enabled) - + .multilineTextAlignment(rtl ? .trailing : .leading) + .padding(.vertical, 6) + .padding(.horizontal, 12) + .overlay(DetermineWidth()) + .frame(minWidth: 0, alignment: .leading) + .textSelection(.enabled) + if let imgWidth = imgWidth, imgWidth < maxWidth { v.frame(maxWidth: imgWidth, alignment: .leading) } else { v } } - + @ViewBuilder private func ciFileView(_ ci: ChatItem, _ text: String) -> some View { CIFileView(file: chatItem.file, edited: chatItem.meta.itemEdited) .overlay(DetermineWidth()) @@ -237,19 +270,29 @@ private struct MetaColorPreferenceKey: PreferenceKey { func onlyImage(_ ci: ChatItem) -> Bool { if case let .image(text, _) = ci.content.msgContent { - return ci.quotedItem == nil && text == "" + return !ci.meta.itemDeleted && ci.quotedItem == nil && text == "" } return false } -func chatItemFrameColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color { +func chatItemFrameColorMaybeImage(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color { onlyImage(ci) ? Color.clear - : ci.chatDir.sent + : chatItemFrameColor(ci, colorScheme) +} + +func chatItemFrameColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color { + ci.chatDir.sent ? (colorScheme == .light ? sentColorLight : sentColorDark) : Color(uiColor: .tertiarySystemGroupedBackground) } +func chatItemFrameContextColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color { + ci.chatDir.sent + ? (colorScheme == .light ? sentQuoteColorLight : sentQuoteColorDark) + : Color(uiColor: .quaternarySystemFill) +} + struct FramedItemView_Previews: PreviewProvider { static var previews: some View { Group{ @@ -266,9 +309,9 @@ struct FramedItemView_Previews: PreviewProvider { } } -struct FramedItemViewEdited_Previews: PreviewProvider { +struct FramedItemView_Edited_Previews: PreviewProvider { static var previews: some View { - Group{ + Group { FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, false, true)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), false, true)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), false, true)) @@ -278,7 +321,26 @@ struct FramedItemViewEdited_Previews: PreviewProvider { FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, false, true)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, false, true)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,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"), false, true)) - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,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"), false, true)) } + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,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"), false, true)) + } + .previewLayout(.fixed(width: 360, height: 200)) + } +} + +struct FramedItemView_Deleted_Previews: PreviewProvider { + static var previews: some View { + Group { + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, true)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), true)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), true)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent, quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), true)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, true)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, true)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, true)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, true)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,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"), true)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,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"), true)) + } .previewLayout(.fixed(width: 360, height: 200)) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift new file mode 100644 index 0000000000..c748ed4df6 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift @@ -0,0 +1,44 @@ +// +// MarkedDeletedItemView.swift +// SimpleX (iOS) +// +// Created by JRoberts on 30.11.2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct MarkedDeletedItemView: View { + @Environment(\.colorScheme) var colorScheme + var chatItem: ChatItem + var showMember = false + + var body: some View { + HStack(alignment: .bottom, spacing: 0) { + if showMember, let member = chatItem.memberDisplayName { + Text(member).fontWeight(.medium) + Text(": ") + } + Text("marked deleted") + .font(.caption) + .foregroundColor(.secondary) + .italic() + CIMetaView(chatItem: chatItem) + .padding(.horizontal, 12) + } + .padding(.leading, 12) + .padding(.vertical, 6) + .background(chatItemFrameColor(chatItem, colorScheme)) + .cornerRadius(18) + .textSelection(.disabled) + } +} + +struct MarkedDeletedItemView_Previews: PreviewProvider { + static var previews: some View { + Group { + MarkedDeletedItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, true, false)) + } + .previewLayout(.fixed(width: 360, height: 200)) + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index 8b743f3533..5ee88536b5 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -15,11 +15,42 @@ struct ChatItemView: View { var showMember = false var maxWidth: CGFloat = .infinity @State var scrollProxy: ScrollViewProxy? = nil + @Binding var revealed: Bool + + var body: some View { + let ci = chatItem + if chatItem.meta.itemDeleted && !revealed { + MarkedDeletedItemView(chatItem: chatItem, showMember: showMember) + } else if ci.quotedItem == nil && !ci.meta.itemDeleted { + if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) { + EmojiItemView(chatItem: ci) + } else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent { + CIVoiceView(chatItem: ci, recordingFile: ci.file, duration: duration) + } else if ci.content.msgContent == nil { + ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, showMember: showMember, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case + } else { + framedItemView() + } + } else { + framedItemView() + } + } + + private func framedItemView() -> some View { + FramedItemView(chatInfo: chatInfo, chatItem: chatItem, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy) + } +} + +struct ChatItemContentView: View { + var chatInfo: ChatInfo + var chatItem: ChatItem + var showMember: Bool + var msgContentView: () -> Content var body: some View { switch chatItem.content { - case .sndMsgContent: contentItemView() - case .rcvMsgContent: contentItemView() + case .sndMsgContent: msgContentView() + case .rcvMsgContent: msgContentView() case .sndDeleted: deletedItemView() case .rcvDeleted: deletedItemView() case let .sndCall(status, duration): callItemView(status, duration) @@ -40,17 +71,6 @@ struct ChatItemView: View { } } - @ViewBuilder private func contentItemView() -> some View { - if (chatItem.quotedItem == nil && chatItem.file == nil && isShortEmoji(chatItem.content.text)) { - EmojiItemView(chatItem: chatItem) - } else if chatItem.quotedItem == nil && chatItem.content.text.isEmpty, - case let .voice(_, duration) = chatItem.content.msgContent { - CIVoiceView(chatItem: chatItem, recordingFile: chatItem.file, duration: duration) - } else { - FramedItemView(chatInfo: chatInfo, chatItem: chatItem, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy) - } - } - private func deletedItemView() -> some View { DeletedItemView(chatItem: chatItem, showMember: showMember) } @@ -75,12 +95,67 @@ struct ChatItemView: View { struct ChatItemView_Previews: PreviewProvider { static var previews: some View { Group{ - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello")) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too")) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂")) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂")) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂")) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getDeletedContentSample()) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getDeletedContentSample(), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, true, false), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, true, false), revealed: Binding.constant(true)) + } + .previewLayout(.fixed(width: 360, height: 70)) + } +} + +struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider { + static var previews: some View { + let ciFeatureContent = CIContent.rcvChatFeature(feature: .fullDelete, enabled: FeatureEnabled(forUser: false, forContact: false)) + Group{ + ChatItemView( + chatInfo: ChatInfo.sampleData.direct, + chatItem: ChatItem( + chatDir: .directRcv, + meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead, true, false, false), + content: .rcvIntegrityError(msgError: .msgSkipped(fromMsgId: 1, toMsgId: 2)), + quotedItem: nil, + file: nil + ), + revealed: Binding.constant(true) + ) + ChatItemView( + chatInfo: ChatInfo.sampleData.direct, + chatItem: ChatItem( + chatDir: .directRcv, + meta: CIMeta.getSample(1, .now, "received invitation to join group team as admin", .rcvRead, true, false, false), + content: .rcvGroupInvitation(groupInvitation: CIGroupInvitation.getSample(status: .pending), memberRole: .admin), + quotedItem: nil, + file: nil + ), + revealed: Binding.constant(true) + ) + ChatItemView( + chatInfo: ChatInfo.sampleData.direct, + chatItem: ChatItem( + chatDir: .directRcv, + meta: CIMeta.getSample(1, .now, "group event text", .rcvRead, true, false, false), + content: .rcvGroupEvent(rcvGroupEvent: .memberAdded(groupMemberId: 1, profile: Profile.sampleData)), + quotedItem: nil, + file: nil + ), + revealed: Binding.constant(true) + ) + ChatItemView( + chatInfo: ChatInfo.sampleData.direct, + chatItem: ChatItem( + chatDir: .directRcv, + meta: CIMeta.getSample(1, .now, ciFeatureContent.text, .rcvRead, true, false, false), + content: ciFeatureContent, + quotedItem: nil, + file: nil + ), + revealed: Binding.constant(true) + ) } .previewLayout(.fixed(width: 360, height: 70)) } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 62366d0c14..6617c7f7dc 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -402,128 +402,180 @@ struct ChatView: View { Rectangle().fill(.clear) .frame(width: memberImageSize, height: memberImageSize) } - chatItemWithMenu(ci, maxWidth, showMember: showMember).padding(.leading, 8) + ChatItemWithMenu( + chat: chat, + ci: ci, + showMember: showMember, + maxWidth: maxWidth, + scrollProxy: scrollProxy, + deleteMessage: deleteMessage, + deletingItem: $deletingItem, + composeState: $composeState, + showDeleteMessage: $showDeleteMessage + ).padding(.leading, 8) } .padding(.trailing) .padding(.leading, 12) } else { - chatItemWithMenu(ci, maxWidth).padding(.horizontal) + ChatItemWithMenu( + chat: chat, + ci: ci, + maxWidth: maxWidth, + scrollProxy: scrollProxy, + deleteMessage: deleteMessage, + deletingItem: $deletingItem, + composeState: $composeState, + showDeleteMessage: $showDeleteMessage + ).padding(.horizontal) } } - private func chatItemWithMenu(_ ci: ChatItem, _ maxWidth: CGFloat, showMember: Bool = false) -> some View { - let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading - var menu: [UIAction] = [] - if let mc = ci.content.msgContent { - menu.append(contentsOf: [ - UIAction( - title: NSLocalizedString("Reply", comment: "chat item action"), - image: UIImage(systemName: "arrowshape.turn.up.left") - ) { _ in - withAnimation { - if composeState.editing { - composeState = ComposeState(contextItem: .quotedItem(chatItem: ci)) - } else { - composeState = composeState.copy(contextItem: .quotedItem(chatItem: ci)) + private struct ChatItemWithMenu: View { + var chat: Chat + var ci: ChatItem + var showMember: Bool = false + var maxWidth: CGFloat + var scrollProxy: ScrollViewProxy? + var deleteMessage: (CIDeleteMode) -> Void + @Binding var deletingItem: ChatItem? + @Binding var composeState: ComposeState + @Binding var showDeleteMessage: Bool + + @State private var revealed = false + + var body: some View { + let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading + + ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed) + .uiKitContextMenu(actions: menu()) + .confirmationDialog("Delete message?", isPresented: $showDeleteMessage, titleVisibility: .visible) { + Button("Delete for me", role: .destructive) { + deleteMessage(.cidmInternal) + } + if let di = deletingItem, di.meta.editable { + Button(broadcastDeleteButtonText, role: .destructive) { + deleteMessage(.cidmBroadcast) } } - }, - UIAction( - title: NSLocalizedString("Share", comment: "chat item action"), - image: UIImage(systemName: "square.and.arrow.up") - ) { _ in - var shareItems: [Any] = [ci.content.text] - if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) { - shareItems.append(image) - } - showShareSheet(items: shareItems) - }, - UIAction( - title: NSLocalizedString("Copy", comment: "chat item action"), - image: UIImage(systemName: "doc.on.doc") - ) { _ in - if case let .image(text, _) = ci.content.msgContent, - text == "", - let image = getLoadedImage(ci.file) { - UIPasteboard.general.image = image - } else { - UIPasteboard.general.string = ci.content.text - } } - ]) - if let filePath = getLoadedFilePath(ci.file) { - if case .image = ci.content.msgContent, - let image = UIImage(contentsOfFile: filePath) { + .frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment) + .frame(minWidth: 0, maxWidth: .infinity, alignment: alignment) + } + + private func menu() -> [UIAction] { + var menu: [UIAction] = [] + if let mc = ci.content.msgContent, !ci.meta.itemDeleted || revealed { + if !ci.meta.itemDeleted { menu.append( UIAction( - title: NSLocalizedString("Save", comment: "chat item action"), - image: UIImage(systemName: "square.and.arrow.down") + title: NSLocalizedString("Reply", comment: "chat item action"), + image: UIImage(systemName: "arrowshape.turn.up.left") ) { _ in - UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) - } - ) - } else { - menu.append( - UIAction( - title: NSLocalizedString("Save", comment: "chat item action"), - image: UIImage(systemName: "square.and.arrow.down") - ) { _ in - let fileURL = URL(fileURLWithPath: filePath) - showShareSheet(items: [fileURL]) + withAnimation { + if composeState.editing { + composeState = ComposeState(contextItem: .quotedItem(chatItem: ci)) + } else { + composeState = composeState.copy(contextItem: .quotedItem(chatItem: ci)) + } + } } ) } - } - if ci.meta.editable, - !mc.isVoice { menu.append( UIAction( - title: NSLocalizedString("Edit", comment: "chat item action"), - image: UIImage(systemName: "square.and.pencil") + title: NSLocalizedString("Share", comment: "chat item action"), + image: UIImage(systemName: "square.and.arrow.up") ) { _ in - withAnimation { - composeState = ComposeState(editingItem: ci) + var shareItems: [Any] = [ci.content.text] + if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) { + shareItems.append(image) + } + showShareSheet(items: shareItems) + } + ) + menu.append( + UIAction( + title: NSLocalizedString("Copy", comment: "chat item action"), + image: UIImage(systemName: "doc.on.doc") + ) { _ in + if case let .image(text, _) = ci.content.msgContent, + text == "", + let image = getLoadedImage(ci.file) { + UIPasteboard.general.image = image + } else { + UIPasteboard.general.string = ci.content.text } } ) - } - menu.append( - UIAction( - title: NSLocalizedString("Delete", comment: "chat item action"), - image: UIImage(systemName: "trash"), - attributes: [.destructive] - ) { _ in - showDeleteMessage = true - deletingItem = ci - } - ) - } else if ci.isDeletedContent { - menu.append( - UIAction( - title: NSLocalizedString("Delete", comment: "chat item action"), - image: UIImage(systemName: "trash"), - attributes: [.destructive] - ) { _ in - showDeleteMessage = true - deletingItem = ci - } - ) - } - - return ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy) - .uiKitContextMenu(actions: menu) - .confirmationDialog("Delete message?", isPresented: $showDeleteMessage, titleVisibility: .visible) { - Button("Delete for me", role: .destructive) { - deleteMessage(.cidmInternal) - } - if let di = deletingItem, di.meta.editable { - Button("Delete for everyone",role: .destructive) { - deleteMessage(.cidmBroadcast) + if let filePath = getLoadedFilePath(ci.file) { + if case .image = ci.content.msgContent, let image = UIImage(contentsOfFile: filePath) { + menu.append( + UIAction( + title: NSLocalizedString("Save", comment: "chat item action"), + image: UIImage(systemName: "square.and.arrow.down") + ) { _ in + UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) + } + ) + } else { + menu.append( + UIAction( + title: NSLocalizedString("Save", comment: "chat item action"), + image: UIImage(systemName: "square.and.arrow.down") + ) { _ in + let fileURL = URL(fileURLWithPath: filePath) + showShareSheet(items: [fileURL]) + } + ) } } + if ci.meta.editable && !mc.isVoice { + menu.append( + UIAction( + title: NSLocalizedString("Edit", comment: "chat item action"), + image: UIImage(systemName: "square.and.pencil") + ) { _ in + withAnimation { + composeState = ComposeState(editingItem: ci) + } + } + ) + } + menu.append(deleteUIAction()) + } else if ci.meta.itemDeleted { + menu.append(revealUIAction()) + menu.append(deleteUIAction()) + } else if ci.isDeletedContent { + menu.append(deleteUIAction()) } - .frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment) - .frame(minWidth: 0, maxWidth: .infinity, alignment: alignment) + return menu + } + + private func deleteUIAction() -> UIAction { + UIAction( + title: NSLocalizedString("Delete", comment: "chat item action"), + image: UIImage(systemName: "trash"), + attributes: [.destructive] + ) { _ in + showDeleteMessage = true + deletingItem = ci + } + } + + private func revealUIAction() -> UIAction { + UIAction( + title: NSLocalizedString("Reveal", comment: "chat item action"), + image: UIImage(systemName: "eye") + ) { _ in + withAnimation { + revealed = true + } + } + } + + private var broadcastDeleteButtonText: LocalizedStringKey { + chat.chatInfo.fullDeletionAllowed ? "Delete for everyone" : "Mark deleted for everyone" + } } private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool { @@ -552,7 +604,7 @@ struct ChatView: View { logger.debug("ChatView deleteMessage: in Task") do { if let di = deletingItem { - let toItem = try await apiDeleteChatItem( + let (deletedItem, toItem) = try await apiDeleteChatItem( type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, itemId: di.id, @@ -560,7 +612,11 @@ struct ChatView: View { ) DispatchQueue.main.async { deletingItem = nil - let _ = chatModel.removeChatItem(chat.chatInfo, toItem) + if let toItem = toItem { + _ = chatModel.upsertChatItem(chat.chatInfo, toItem) + } else { + chatModel.removeChatItem(chat.chatInfo, deletedItem) + } } } } catch { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift index bd0929e567..a9847afe4d 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift @@ -16,10 +16,6 @@ struct ContextItemView: View { let cancelContextItem: () -> Void var body: some View { - let bgColor = contextItem.chatDir.sent - ? (colorScheme == .light ? sentColorLight : sentColorDark) - : Color(uiColor: .tertiarySystemGroupedBackground) - HStack { Image(systemName: contextIcon) .resizable() @@ -45,7 +41,7 @@ struct ContextItemView: View { .padding(12) .frame(minHeight: 50) .frame(maxWidth: .infinity) - .background(bgColor) + .background(chatItemFrameColor(contextItem, colorScheme)) .padding(.top, 8) } } diff --git a/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift b/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift index 30f33a5730..3d5afb4879 100644 --- a/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift @@ -20,7 +20,7 @@ struct ContactPreferencesView: View { VStack { List { - // featureSection(.fullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, $featuresAllowed.fullDelete) + featureSection(.fullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, $featuresAllowed.fullDelete) featureSection(.voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, $featuresAllowed.voice) Section { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift index 73039a1bca..842faad9dc 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift @@ -18,7 +18,7 @@ struct GroupPreferencesView: View { var body: some View { VStack { List { - // featureSection(.fullDelete, $preferences.fullDelete.enable) + featureSection(.fullDelete, $preferences.fullDelete.enable) featureSection(.directMessages, $preferences.directMessages.enable) featureSection(.voice, $preferences.voice.enable) diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index e56780c38f..1be0d070a0 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -101,8 +101,10 @@ struct ChatPreviewView: View { @ViewBuilder private func chatPreviewText(_ cItem: ChatItem?) -> some View { if let cItem = cItem { + let itemText = !cItem.meta.itemDeleted ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text") + let itemFormattedText = !cItem.meta.itemDeleted ? cItem.formattedText : nil ZStack(alignment: .topTrailing) { - (itemStatusMark(cItem) + messageText(cItem.text, cItem.formattedText, cItem.memberDisplayName, preview: true)) + (itemStatusMark(cItem) + messageText(itemText, itemFormattedText, cItem.memberDisplayName, preview: true)) .lineLimit(2) .multilineTextAlignment(.leading) .frame(maxWidth: .infinity, alignment: .topLeading) @@ -204,6 +206,10 @@ struct ChatPreviewView_Previews: PreviewProvider { chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)], chatStats: ChatStats(unreadCount: 11, minUnreadItemId: 0) )) + ChatPreviewView(chat: Chat( + chatInfo: ChatInfo.sampleData.direct, + chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, true, false)] + )) ChatPreviewView(chat: Chat( chatInfo: ChatInfo.sampleData.direct, chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)], diff --git a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift index 6d24cf4475..90907d59ad 100644 --- a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift +++ b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift @@ -18,7 +18,7 @@ struct PreferencesView: View { var body: some View { VStack { List { - // featureSection(.fullDelete, $preferences.fullDelete.allow) + featureSection(.fullDelete, $preferences.fullDelete.allow) featureSection(.voice, $preferences.voice.allow) Section { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 90c69fc2aa..af0f2f093e 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -74,11 +74,6 @@ 5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059C4279559F40002BEB4 /* ContentView.swift */; }; 5CA059EF279559F40002BEB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5CA059C5279559F40002BEB4 /* Assets.xcassets */; }; 5CA7DFC329302AF000F7FDDE /* AppSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA7DFC229302AF000F7FDDE /* AppSheet.swift */; }; - 5CA7DFD32933E16C00F7FDDE /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA7DFCE2933E16B00F7FDDE /* libffi.a */; }; - 5CA7DFD42933E16C00F7FDDE /* libHSsimplex-chat-4.3.0-KONDmy6IJf0HwVSmQIx39W-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA7DFCF2933E16B00F7FDDE /* libHSsimplex-chat-4.3.0-KONDmy6IJf0HwVSmQIx39W-ghc8.10.7.a */; }; - 5CA7DFD52933E16C00F7FDDE /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA7DFD02933E16C00F7FDDE /* libgmp.a */; }; - 5CA7DFD62933E16C00F7FDDE /* libHSsimplex-chat-4.3.0-KONDmy6IJf0HwVSmQIx39W.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA7DFD12933E16C00F7FDDE /* libHSsimplex-chat-4.3.0-KONDmy6IJf0HwVSmQIx39W.a */; }; - 5CA7DFD72933E16C00F7FDDE /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA7DFD22933E16C00F7FDDE /* libgmpxx.a */; }; 5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79929211BB900072E13 /* PreferencesView.swift */; }; 5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */; }; 5CB0BA882826CB3A00B3292C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CB0BA862826CB3A00B3292C /* InfoPlist.strings */; }; @@ -141,6 +136,12 @@ 644EFFDE292BCD9D00525D5B /* ComposeVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */; }; 644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */; }; 644EFFE2292D089800525D5B /* FramedCIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */; }; + 644EFFE42937BE9700525D5B /* MarkedDeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */; }; + 644EFFEC293B5B1800525D5B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644EFFE7293B5B1700525D5B /* libgmpxx.a */; }; + 644EFFED293B5B1800525D5B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644EFFE8293B5B1700525D5B /* libgmp.a */; }; + 644EFFEE293B5B1800525D5B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644EFFE9293B5B1800525D5B /* libffi.a */; }; + 644EFFEF293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644EFFEA293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X-ghc8.10.7.a */; }; + 644EFFF0293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644EFFEB293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X.a */; }; 6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; }; 646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */; }; 646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; }; @@ -284,11 +285,6 @@ 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = ""; }; 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOSLaunchTests.swift; sourceTree = ""; }; 5CA7DFC229302AF000F7FDDE /* AppSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSheet.swift; sourceTree = ""; }; - 5CA7DFCE2933E16B00F7FDDE /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5CA7DFCF2933E16B00F7FDDE /* libHSsimplex-chat-4.3.0-KONDmy6IJf0HwVSmQIx39W-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.3.0-KONDmy6IJf0HwVSmQIx39W-ghc8.10.7.a"; sourceTree = ""; }; - 5CA7DFD02933E16C00F7FDDE /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5CA7DFD12933E16C00F7FDDE /* libHSsimplex-chat-4.3.0-KONDmy6IJf0HwVSmQIx39W.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.3.0-KONDmy6IJf0HwVSmQIx39W.a"; sourceTree = ""; }; - 5CA7DFD22933E16C00F7FDDE /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 5CADE79929211BB900072E13 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = ""; }; 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactPreferencesView.swift; sourceTree = ""; }; 5CB0BA872826CB3A00B3292C /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -351,6 +347,12 @@ 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeVoiceView.swift; sourceTree = ""; }; 644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIVoiceView.swift; sourceTree = ""; }; 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramedCIVoiceView.swift; sourceTree = ""; }; + 644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkedDeletedItemView.swift; sourceTree = ""; }; + 644EFFE7293B5B1700525D5B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 644EFFE8293B5B1700525D5B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 644EFFE9293B5B1800525D5B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 644EFFEA293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X-ghc8.10.7.a"; sourceTree = ""; }; + 644EFFEB293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X.a"; sourceTree = ""; }; 6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = ""; }; 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/System/Library/Frameworks/LocalAuthentication.framework; sourceTree = DEVELOPER_DIR; }; 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = ""; }; @@ -396,13 +398,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 644EFFEC293B5B1800525D5B /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5CA7DFD52933E16C00F7FDDE /* libgmp.a in Frameworks */, - 5CA7DFD62933E16C00F7FDDE /* libHSsimplex-chat-4.3.0-KONDmy6IJf0HwVSmQIx39W.a in Frameworks */, - 5CA7DFD42933E16C00F7FDDE /* libHSsimplex-chat-4.3.0-KONDmy6IJf0HwVSmQIx39W-ghc8.10.7.a in Frameworks */, - 5CA7DFD32933E16C00F7FDDE /* libffi.a in Frameworks */, + 644EFFED293B5B1800525D5B /* libgmp.a in Frameworks */, + 644EFFF0293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X.a in Frameworks */, + 644EFFEE293B5B1800525D5B /* libffi.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5CA7DFD72933E16C00F7FDDE /* libgmpxx.a in Frameworks */, + 644EFFEF293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X-ghc8.10.7.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -458,11 +460,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CA7DFCE2933E16B00F7FDDE /* libffi.a */, - 5CA7DFD02933E16C00F7FDDE /* libgmp.a */, - 5CA7DFD22933E16C00F7FDDE /* libgmpxx.a */, - 5CA7DFCF2933E16B00F7FDDE /* libHSsimplex-chat-4.3.0-KONDmy6IJf0HwVSmQIx39W-ghc8.10.7.a */, - 5CA7DFD12933E16C00F7FDDE /* libHSsimplex-chat-4.3.0-KONDmy6IJf0HwVSmQIx39W.a */, + 644EFFE9293B5B1800525D5B /* libffi.a */, + 644EFFE8293B5B1700525D5B /* libgmp.a */, + 644EFFE7293B5B1700525D5B /* libgmpxx.a */, + 644EFFEA293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X-ghc8.10.7.a */, + 644EFFEB293B5B1800525D5B /* libHSsimplex-chat-4.3.0-GPZqasc9wbbBzOZyUtfq0X.a */, ); path = Libraries; sourceTree = ""; @@ -679,6 +681,7 @@ 6440C9FF288857A10062C672 /* CIEventView.swift */, 5C58BCD5292BEBE600AF9E4F /* CIChatFeatureView.swift */, 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */, + 644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */, ); path = ChatItem; sourceTree = ""; @@ -1022,6 +1025,7 @@ 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */, 5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */, 5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */, + 644EFFE42937BE9700525D5B /* MarkedDeletedItemView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 7c54711c8a..1615c5c445 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -332,7 +332,7 @@ public enum ChatResponse: Decodable, Error { case newChatItem(chatItem: AChatItem) case chatItemStatusUpdated(chatItem: AChatItem) case chatItemUpdated(chatItem: AChatItem) - case chatItemDeleted(deletedChatItem: AChatItem, toChatItem: AChatItem) + case chatItemDeleted(deletedChatItem: AChatItem, toChatItem: AChatItem?, byUser: Bool) case contactsList(contacts: [Contact]) // group events case groupCreated(groupInfo: GroupInfo) @@ -538,7 +538,7 @@ public enum ChatResponse: Decodable, Error { case let .newChatItem(chatItem): return String(describing: chatItem) case let .chatItemStatusUpdated(chatItem): return String(describing: chatItem) case let .chatItemUpdated(chatItem): return String(describing: chatItem) - case let .chatItemDeleted(deletedChatItem, toChatItem): return "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))" + case let .chatItemDeleted(deletedChatItem, toChatItem, byUser): return "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))\nbyUser: \(byUser)" case let .contactsList(contacts): return String(describing: contacts) case let .groupCreated(groupInfo): return String(describing: groupInfo) case let .sentGroupInvitation(groupInfo, contact, member): return "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)" diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 0e6dcf334a..b87a10a6d7 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -54,7 +54,7 @@ public struct Profile: Codable, NamedChat { (fullName == "" || displayName == fullName) ? displayName : "\(displayName) (\(fullName))" } - static let sampleData = Profile( + public static let sampleData = Profile( displayName: "alice", fullName: "Alice" ) @@ -695,7 +695,15 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.mergedPreferences.voice.enabled.forUser case let .group(groupInfo): return groupInfo.fullGroupPreferences.voice.on - default: return true + default: return false + } + } + + public var fullDeletionAllowed: Bool { + switch self { + case let .direct(contact): return contact.mergedPreferences.fullDelete.enabled.forUser + case let .group(groupInfo): return groupInfo.fullGroupPreferences.fullDelete.on + default: return false } } @@ -1366,6 +1374,7 @@ public struct ChatItem: Identifiable, Decodable { public var timestampText: Text { meta.timestampText } + // pair with formattedText public var text: String { switch (content.text, content.msgContent, file) { case let ("", .some(.voice(_, duration)), _): return "Voice message (\(durationText(duration)))" @@ -1766,6 +1775,13 @@ public enum MsgContent { } } + public var isText: Bool { + switch self { + case .text: return true + default: return false + } + } + public var isVoice: Bool { switch self { case .voice: return true diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 0522c215e7..e75e67e7c8 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -325,17 +325,18 @@ processChatCommand = \case prepareMsg fileInvitation_ = case quotedItemId_ of Nothing -> pure (MCSimple (ExtMsgContent mc fileInvitation_), Nothing) Just quotedItemId -> do - CChatItem _ ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, content = ciContent, formattedText, file} <- + CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <- withStore $ \db -> getDirectChatItem db userId chatId quotedItemId - (origQmc, qd, sent) <- quoteData ciContent + (origQmc, qd, sent) <- quoteData qci let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing} qmc = quoteContent origQmc file quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText} pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fileInvitation_), Just quotedItem) where - quoteData :: CIContent d -> m (MsgContent, CIQDirection 'CTDirect, Bool) - quoteData (CISndMsgContent qmc) = pure (qmc, CIQDirectSnd, True) - quoteData (CIRcvMsgContent qmc) = pure (qmc, CIQDirectRcv, False) + quoteData :: ChatItem c d -> m (MsgContent, CIQDirection 'CTDirect, Bool) + quoteData ChatItem {meta = CIMeta {itemDeleted = True}} = throwChatError CEInvalidQuote + quoteData ChatItem {content = CISndMsgContent qmc} = pure (qmc, CIQDirectSnd, True) + quoteData ChatItem {content = CIRcvMsgContent qmc} = pure (qmc, CIQDirectRcv, False) quoteData _ = throwChatError CEInvalidQuote CTGroup -> do Group gInfo@GroupInfo {membership, localDisplayName = gName} ms <- withStore $ \db -> getGroup db user chatId @@ -373,18 +374,19 @@ processChatCommand = \case prepareMsg fileInvitation_ membership = case quotedItemId_ of Nothing -> pure (MCSimple (ExtMsgContent mc fileInvitation_), Nothing) Just quotedItemId -> do - CChatItem _ ChatItem {chatDir, meta = CIMeta {itemTs, itemSharedMsgId}, content = ciContent, formattedText, file} <- + CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <- withStore $ \db -> getGroupChatItem db user chatId quotedItemId - (origQmc, qd, sent, GroupMember {memberId}) <- quoteData ciContent chatDir membership + (origQmc, qd, sent, GroupMember {memberId}) <- quoteData qci membership let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Just memberId} qmc = quoteContent origQmc file quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc, formattedText} pure (MCQuote QuotedMsg {msgRef, content = qmc} (ExtMsgContent mc fileInvitation_), Just quotedItem) where - quoteData :: CIContent d -> CIDirection 'CTGroup d -> GroupMember -> m (MsgContent, CIQDirection 'CTGroup, Bool, GroupMember) - quoteData (CISndMsgContent qmc) CIGroupSnd membership' = pure (qmc, CIQGroupSnd, True, membership') - quoteData (CIRcvMsgContent qmc) (CIGroupRcv m) _ = pure (qmc, CIQGroupRcv $ Just m, False, m) - quoteData _ _ _ = throwChatError CEInvalidQuote + quoteData :: ChatItem c d -> GroupMember -> m (MsgContent, CIQDirection 'CTGroup, Bool, GroupMember) + quoteData ChatItem {meta = CIMeta {itemDeleted = True}} _ = throwChatError CEInvalidQuote + quoteData ChatItem {chatDir = CIGroupSnd, content = CISndMsgContent qmc} membership' = pure (qmc, CIQGroupSnd, True, membership') + quoteData ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent qmc} _ = pure (qmc, CIQGroupRcv $ Just m, False, m) + quoteData _ _ = throwChatError CEInvalidQuote CTContactRequest -> pure $ chatCmdError "not supported" CTContactConnection -> pure $ chatCmdError "not supported" where diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 99294c6ad9..1190dfd01f 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -267,7 +267,7 @@ mkCIMeta :: ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe SharedMsgId mkCIMeta itemId itemContent itemText itemStatus itemSharedMsgId itemDeleted itemEdited tz currentTs itemTs createdAt updatedAt = let localItemTs = utcToZonedTime tz itemTs editable = case itemContent of - CISndMsgContent _ -> diffUTCTime currentTs itemTs < nominalDay + CISndMsgContent _ -> diffUTCTime currentTs itemTs < nominalDay && not itemDeleted _ -> False in CIMeta {itemId, itemTs, itemText, itemStatus, itemSharedMsgId, itemDeleted, itemEdited, editable, localItemTs, createdAt, updatedAt} diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 9776910c25..c579b28296 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -3830,7 +3830,7 @@ markDirectChatItemDeleted db User {userId} ct@Contact {contactId} (CChatItem msg WHERE user_id = ? AND contact_id = ? AND chat_item_id = ? |] (currentTs, userId, contactId, itemId) - pure $ AChatItem SCTDirect msgDir (DirectChat ct) (ci {meta = (meta ci) {itemDeleted = True}}) + pure $ AChatItem SCTDirect msgDir (DirectChat ct) (ci {meta = (meta ci) {itemDeleted = True, editable = False}}) getDirectChatItemBySharedMsgId :: DB.Connection -> UserId -> ContactId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTDirect) getDirectChatItemBySharedMsgId db userId contactId sharedMsgId = do @@ -3939,7 +3939,7 @@ markGroupChatItemDeleted db User {userId} gInfo@GroupInfo {groupId} (CChatItem m WHERE user_id = ? AND group_id = ? AND chat_item_id = ? |] (currentTs, userId, groupId, itemId) - pure $ AChatItem SCTGroup msgDir (GroupChat gInfo) (ci {meta = (meta ci) {itemDeleted = True}}) + pure $ AChatItem SCTGroup msgDir (GroupChat gInfo) (ci {meta = (meta ci) {itemDeleted = True, editable = False}}) getGroupChatItemBySharedMsgId :: DB.Connection -> User -> GroupId -> GroupMemberId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTGroup) getGroupChatItemBySharedMsgId db user@User {userId} groupId groupMemberId sharedMsgId = do