From 63f344bde63ae00d06fa172df9795c6a6e6299b4 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 9 May 2023 20:43:21 +0400 Subject: [PATCH] ios: view edit history; core: prohibit item updates w/t changes (#2413) * ios: view edit history; core: prohibit item updates w/t changes * read more less wip * Revert "read more less wip" This reverts commit 8e0663377bbc526c5a1ed27b4234e382d16f2557. * comment for translations --- apps/ios/Shared/Model/SimpleXAPI.swift | 6 + .../Views/Chat/ChatItem/FramedItemView.swift | 6 +- .../Shared/Views/Chat/ChatItemInfoView.swift | 147 ++++++++++++++++++ apps/ios/Shared/Views/Chat/ChatView.swift | 27 ++++ apps/ios/SimpleX.xcodeproj/project.pbxproj | 4 + apps/ios/SimpleXChat/APITypes.swift | 6 + apps/ios/SimpleXChat/ChatTypes.swift | 15 ++ src/Simplex/Chat.hs | 63 ++++---- tests/ChatTests/Groups.hs | 15 +- 9 files changed, 254 insertions(+), 35 deletions(-) create mode 100644 apps/ios/Shared/Views/Chat/ChatItemInfoView.swift diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index f616a9929..dd13fa3d7 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -295,6 +295,12 @@ func loadChat(chat: Chat, search: String = "") { } } +func apiGetChatItemInfo(itemId: Int64) async throws -> ChatItemInfo { + let r = await chatSendCmd(.apiGetChatItemInfo(itemId: itemId)) + if case let .chatItemInfo(_, _, chatItemInfo) = r { return chatItemInfo } + throw r +} + func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false) async -> ChatItem? { let chatModel = ChatModel.shared let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index d5ed8a67f..ac9b7236c 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -327,7 +327,11 @@ func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ colorScheme: ColorSch } func chatItemFrameColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color { - ci.chatDir.sent + ciDirFrameColor(chatItemSent: ci.chatDir.sent, colorScheme: colorScheme) +} + +func ciDirFrameColor(chatItemSent: Bool, colorScheme: ColorScheme) -> Color { + chatItemSent ? (colorScheme == .light ? sentColorLight : sentColorDark) : Color(uiColor: .tertiarySystemGroupedBackground) } diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift new file mode 100644 index 000000000..748b6a83c --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -0,0 +1,147 @@ +// +// ChatItemInfoView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 09.05.2023. +// Copyright © 2023 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ChatItemInfoView: View { + @Environment(\.colorScheme) var colorScheme + var chatItemSent: Bool + @Binding var chatItemInfo: ChatItemInfo? + @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false + + var body: some View { + if let chatItemInfo = chatItemInfo { + NavigationView { + itemInfoView(chatItemInfo) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { showShareSheet(items: [itemInfoShareText(chatItemInfo)]) } label: { + Image(systemName: "square.and.arrow.up") + } + } + } + } + } else { + Text("No message details") + } + } + + @ViewBuilder private func itemInfoView(_ chatItemInfo: ChatItemInfo) -> some View { + GeometryReader { g in + ScrollView { + VStack(alignment: .leading, spacing: 12) { + Text("Message details") + .font(.largeTitle) + .bold() + .padding(.bottom) + + let maxWidth = (g.size.width - 32) * 0.84 + if developerTools { + infoRow("Database ID", "\(chatItemInfo.chatItemId)") + } + infoRow("Sent at", localTimestamp(chatItemInfo.itemTs)) + if !chatItemSent { + infoRow("Received at", localTimestamp(chatItemInfo.createdAt)) + } + + if !chatItemInfo.itemVersions.isEmpty { + Divider() + .padding(.top) + + Text("Edit history") + .font(.title) + .padding(.bottom, 4) + LazyVStack(alignment: .leading, spacing: 12) { + ForEach(Array(chatItemInfo.itemVersions.enumerated()), id: \.element.chatItemVersionId) { index, itemVersion in + itemVersionView(itemVersion, maxWidth, current: index == 0) + } + } + } + } + } + .padding() + .frame(maxHeight: .infinity, alignment: .top) + } + } + + @ViewBuilder private func itemVersionView(_ itemVersion: ChatItemVersion, _ maxWidth: CGFloat, current: Bool) -> some View { + let uiMenu: Binding = Binding( + get: { UIMenu(title: "", children: itemVersionMenu(itemVersion)) }, + set: { _ in } + ) + VStack(alignment: .leading, spacing: 4) { + messageText(itemVersion.msgContent.text, parseSimpleXMarkdown(itemVersion.msgContent.text), nil) + .allowsHitTesting(false) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(ciDirFrameColor(chatItemSent: chatItemSent, colorScheme: colorScheme)) + .cornerRadius(18) + .uiKitContextMenu(menu: uiMenu) + Text( + localTimestamp(itemVersion.itemVersionTs) + + (current + ? (" (" + NSLocalizedString("Current", comment: "designation of the current version of the message") + ")") + : "") + ) + .foregroundStyle(.secondary) + .font(.caption) + .padding(.horizontal, 12) + } + .frame(maxWidth: maxWidth, alignment: .leading) + } + + func itemVersionMenu(_ itemVersion: ChatItemVersion) -> [UIAction] {[ + UIAction( + title: NSLocalizedString("Share", comment: "chat item action"), + image: UIImage(systemName: "square.and.arrow.up") + ) { _ in + showShareSheet(items: [itemVersion.msgContent.text]) + }, + UIAction( + title: NSLocalizedString("Copy", comment: "chat item action"), + image: UIImage(systemName: "doc.on.doc") + ) { _ in + UIPasteboard.general.string = itemVersion.msgContent.text + } + ]} + + func itemInfoShareText(_ chatItemInfo: ChatItemInfo) -> String { + var shareText = "" + let nl = "\n" + shareText += "Message details" + nl + nl + if developerTools { + shareText += "Database ID: \(chatItemInfo.chatItemId)" + nl + } + shareText += "Sent at: \(localTimestamp(chatItemInfo.itemTs))" + nl + if !chatItemSent { + shareText += "Received at: \(localTimestamp(chatItemInfo.createdAt))" + nl + } + if !chatItemInfo.itemVersions.isEmpty { + shareText += nl + "Edit history" + nl + nl + for (index, itemVersion) in chatItemInfo.itemVersions.enumerated() { + shareText += localTimestamp(itemVersion.itemVersionTs) + (index == 0 ? " (Current)" : "") + ":" + nl + shareText += itemVersion.msgContent.text + nl + nl + } + } + return shareText.trimmingCharacters(in: .newlines) + } +} + +func localTimestamp(_ date: Date) -> String { + let localDateFormatter = DateFormatter() + localDateFormatter.dateStyle = .medium + localDateFormatter.timeStyle = .medium + return localDateFormatter.string(from: date) +} + +struct ChatItemInfoView_Previews: PreviewProvider { + static var previews: some View { + ChatItemInfoView(chatItemSent: true, chatItemInfo: Binding.constant(nil)) + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index b008d420a..55f8fa5c6 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -445,6 +445,8 @@ struct ChatView: View { @Binding var showDeleteMessage: Bool @State private var revealed = false + @State private var showChatItemInfoSheet: Bool = false + @State private var chatItemInfo: ChatItemInfo? var body: some View { let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading @@ -467,6 +469,11 @@ struct ChatView: View { } .frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment) .frame(minWidth: 0, maxWidth: .infinity, alignment: alignment) + .sheet(isPresented: $showChatItemInfoSheet, onDismiss: { + chatItemInfo = nil + }) { + ChatItemInfoView(chatItemSent: ci.chatDir.sent, chatItemInfo: $chatItemInfo) + } } private func menu(live: Bool) -> [UIAction] { @@ -491,6 +498,7 @@ struct ChatView: View { if ci.meta.editable && !mc.isVoice && !live { menu.append(editAction()) } + menu.append(viewInfoUIAction()) if revealed { menu.append(hideUIAction()) } @@ -589,6 +597,25 @@ struct ChatView: View { } } + private func viewInfoUIAction() -> UIAction { + UIAction( + title: NSLocalizedString("View details", comment: "chat item action"), + image: UIImage(systemName: "info") + ) { _ in + Task { + do { + let ciInfo = try await apiGetChatItemInfo(itemId: ci.id) + await MainActor.run { + chatItemInfo = ciInfo + } + } catch let error { + logger.error("apiGetChatItemInfo error: \(responseError(error))") + } + await MainActor.run { showChatItemInfoSheet = true } + } + } + } + private func cancelFileUIAction(_ fileId: Int64, _ cancelAction: CancelAction) -> UIAction { return UIAction( title: cancelAction.uiAction, diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 837105364..de0fde24a 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -172,6 +172,7 @@ 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; }; 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; }; 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; }; + 64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; 64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; }; @@ -442,6 +443,7 @@ 649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = ""; }; 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = ""; }; 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = ""; }; + 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = ""; }; @@ -548,6 +550,7 @@ 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */, 5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */, 5CBE6C132944CC12002D9531 /* ScanCodeView.swift */, + 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */, ); path = Chat; sourceTree = ""; @@ -1058,6 +1061,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */, 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */, 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */, 5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */, diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 561bbc48d..e29b309a3 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -36,6 +36,7 @@ public enum ChatCommand { case apiStorageEncryption(config: DBEncryptionConfig) case apiGetChats(userId: Int64) case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String) + case apiGetChatItemInfo(itemId: Int64) case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent, live: Bool) case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool) case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) @@ -141,6 +142,7 @@ public enum ChatCommand { case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on" case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)") + case let .apiGetChatItemInfo(itemId): return "/_get item info \(itemId)" case let .apiSendMessage(type, id, file, quotedItemId, mc, live): let msg = encodeJSON(ComposedMessage(filePath: file, quotedItemId: quotedItemId, msgContent: mc)) return "/_send \(ref(type, id)) live=\(onOff(live)) json \(msg)" @@ -247,6 +249,7 @@ public enum ChatCommand { case .apiStorageEncryption: return "apiStorageEncryption" case .apiGetChats: return "apiGetChats" case .apiGetChat: return "apiGetChat" + case .apiGetChatItemInfo: return "apiGetChatItemInfo" case .apiSendMessage: return "apiSendMessage" case .apiUpdateChatItem: return "apiUpdateChatItem" case .apiDeleteChatItem: return "apiDeleteChatItem" @@ -385,6 +388,7 @@ public enum ChatResponse: Decodable, Error { case chatSuspended case apiChats(user: User, chats: [ChatData]) case apiChat(user: User, chat: ChatData) + case chatItemInfo(user: User, chatItem: AChatItem, chatItemInfo: ChatItemInfo) case userProtoServers(user: User, servers: UserProtoServers) case serverTestResult(user: User, testServer: String, testFailure: ProtocolTestFailure?) case chatItemTTL(user: User, chatItemTTL: Int64?) @@ -501,6 +505,7 @@ public enum ChatResponse: Decodable, Error { case .chatSuspended: return "chatSuspended" case .apiChats: return "apiChats" case .apiChat: return "apiChat" + case .chatItemInfo: return "chatItemInfo" case .userProtoServers: return "userProtoServers" case .serverTestResult: return "serverTestResult" case .chatItemTTL: return "chatItemTTL" @@ -616,6 +621,7 @@ public enum ChatResponse: Decodable, Error { case .chatSuspended: return noDetails case let .apiChats(u, chats): return withUser(u, String(describing: chats)) case let .apiChat(u, chat): return withUser(u, String(describing: chat)) + case let .chatItemInfo(u, chatItem, chatItemInfo): return withUser(u, "chatItem: \(String(describing: chatItem))\nchatItemInfo: \(String(describing: chatItemInfo))") case let .userProtoServers(u, servers): return withUser(u, "servers: \(String(describing: servers))") case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))") case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL)) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index f82bb844b..492758fa0 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2857,3 +2857,18 @@ public enum ChatItemTTL: Hashable, Identifiable, Comparable { return lhs.comparisonValue < rhs.comparisonValue } } + +public struct ChatItemInfo: Decodable { + public var chatItemId: Int64 + public var itemTs: Date + public var createdAt: Date + public var updatedAt: Date + public var itemVersions: [ChatItemVersion] +} + +public struct ChatItemVersion: Decodable { + public var chatItemVersionId: Int64 + public var msgContent: MsgContent + public var itemVersionTs: Date + public var createdAt: Date +} diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index e049efcc1..8e911d9f2 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -650,15 +650,18 @@ processChatCommand = \case case cci of CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive}, content = ciContent} -> do case (ciContent, itemSharedMsgId) of - (CISndMsgContent oldMC, Just itemSharedMId) -> do - (SndMessage {msgId}, _) <- sendDirectContactMessage ct (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) - ci' <- withStore' $ \db -> do - currentTs <- liftIO getCurrentTime - addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) - updateDirectChatItem' db user contactId ci (CISndMsgContent mc) live $ Just msgId - startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' - setActive $ ActiveC c - pure $ CRChatItemUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci') + (CISndMsgContent oldMC, Just itemSharedMId) -> + if mc /= oldMC + then do + (SndMessage {msgId}, _) <- sendDirectContactMessage ct (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) + ci' <- withStore' $ \db -> do + currentTs <- liftIO getCurrentTime + addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) + updateDirectChatItem' db user contactId ci (CISndMsgContent mc) live $ Just msgId + startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' + setActive $ ActiveC c + pure $ CRChatItemUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci') + else throwChatError CEInvalidChatItemUpdate _ -> throwChatError CEInvalidChatItemUpdate CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate CTGroup -> do @@ -668,15 +671,18 @@ processChatCommand = \case case cci of CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive}, content = ciContent} -> do case (ciContent, itemSharedMsgId) of - (CISndMsgContent oldMC, Just itemSharedMId) -> do - SndMessage {msgId} <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) - ci' <- withStore' $ \db -> do - currentTs <- liftIO getCurrentTime - addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) - updateGroupChatItem db user groupId ci (CISndMsgContent mc) live $ Just msgId - startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' - setActive $ ActiveG gName - pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci') + (CISndMsgContent oldMC, Just itemSharedMId) -> + if mc /= oldMC + then do + SndMessage {msgId} <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) + ci' <- withStore' $ \db -> do + currentTs <- liftIO getCurrentTime + addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) + updateGroupChatItem db user groupId ci (CISndMsgContent mc) live $ Just msgId + startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' + setActive $ ActiveG gName + pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci') + else throwChatError CEInvalidChatItemUpdate _ -> throwChatError CEInvalidChatItemUpdate CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate CTContactRequest -> pure $ chatCmdError (Just user) "not supported" @@ -3324,12 +3330,15 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do updateRcvChatItem = do cci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId case cci of - CChatItem SMDRcv ci@ChatItem {content = CIRcvMsgContent oldMC} -> do - ci' <- withStore' $ \db -> do - addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc) - updateDirectChatItem' db user contactId ci content live $ Just msgId - toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') - startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' + CChatItem SMDRcv ci@ChatItem {content = CIRcvMsgContent oldMC} -> + if mc /= oldMC + then do + ci' <- withStore' $ \db -> do + addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc) + updateDirectChatItem' db user contactId ci content live $ Just msgId + toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') + startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' + else messageError "x.msg.update: contact attempted invalid message update" _ -> messageError "x.msg.update: contact attempted invalid message update" messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> m () @@ -3393,8 +3402,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do updateRcvChatItem = do cci <- withStore $ \db -> getGroupChatItemBySharedMsgId db user groupId groupMemberId sharedMsgId case cci of - CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv m', content = CIRcvMsgContent oldMC} -> do - if sameMemberId memberId m' + CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv m', content = CIRcvMsgContent oldMC} -> + if sameMemberId memberId m' && mc /= oldMC then do ci' <- withStore' $ \db -> do addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc) @@ -3402,7 +3411,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') setActive $ ActiveG g startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' - else messageError "x.msg.update: group member attempted to update a message of another member" -- shouldn't happen now that query includes group member id + else messageError "x.msg.update: group member attempted invalid message update" _ -> messageError "x.msg.update: group member attempted invalid message update" groupMessageDelete :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe MemberId -> RcvMessage -> m () diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index e2cf4e2e6..1c907cd26 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -1044,6 +1044,7 @@ testGroupLiveMessage = bob <# "#team alice> [LIVE ended] hello there" cath <# "#team alice> [LIVE ended] hello there" -- empty live message is also sent instantly + threadDelay 1000000 alice `send` "/live #team" msgItemId2 <- lastItemId alice bob <#. "#team alice> [LIVE started]" @@ -1058,13 +1059,13 @@ testGroupLiveMessage = alice <## "message history:" alice .<## ": hello 2" alice .<## ":" - -- bobItemId <- lastItemId bob - -- bob ##> ("/_get item info " <> bobItemId) - -- bob <##. "sent at: " - -- bob <##. "received at: " - -- bob <## "message history:" - -- bob .<## ": hello 2" - -- bob .<## ":" + bobItemId <- lastItemId bob + bob ##> ("/_get item info " <> bobItemId) + bob <##. "sent at: " + bob <##. "received at: " + bob <## "message history:" + bob .<## ": hello 2" + bob .<## ":" testUpdateGroupProfile :: HasCallStack => FilePath -> IO () testUpdateGroupProfile =