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 8e0663377b.

* comment for translations
This commit is contained in:
spaced4ndy 2023-05-09 20:43:21 +04:00 committed by GitHub
parent 0b8d9d11e2
commit 63f344bde6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 254 additions and 35 deletions

View File

@ -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? { func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false) async -> ChatItem? {
let chatModel = ChatModel.shared let chatModel = ChatModel.shared
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live) let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live)

View File

@ -327,7 +327,11 @@ func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ colorScheme: ColorSch
} }
func chatItemFrameColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color { 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) ? (colorScheme == .light ? sentColorLight : sentColorDark)
: Color(uiColor: .tertiarySystemGroupedBackground) : Color(uiColor: .tertiarySystemGroupedBackground)
} }

View File

@ -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<UIMenu> = 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))
}
}

View File

@ -445,6 +445,8 @@ struct ChatView: View {
@Binding var showDeleteMessage: Bool @Binding var showDeleteMessage: Bool
@State private var revealed = false @State private var revealed = false
@State private var showChatItemInfoSheet: Bool = false
@State private var chatItemInfo: ChatItemInfo?
var body: some View { var body: some View {
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
@ -467,6 +469,11 @@ struct ChatView: View {
} }
.frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment) .frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment)
.frame(minWidth: 0, maxWidth: .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] { private func menu(live: Bool) -> [UIAction] {
@ -491,6 +498,7 @@ struct ChatView: View {
if ci.meta.editable && !mc.isVoice && !live { if ci.meta.editable && !mc.isVoice && !live {
menu.append(editAction()) menu.append(editAction())
} }
menu.append(viewInfoUIAction())
if revealed { if revealed {
menu.append(hideUIAction()) 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 { private func cancelFileUIAction(_ fileId: Int64, _ cancelAction: CancelAction) -> UIAction {
return UIAction( return UIAction(
title: cancelAction.uiAction, title: cancelAction.uiAction,

View File

@ -172,6 +172,7 @@
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; }; 649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; }; 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.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 */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.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 = "<group>"; }; 649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; }; 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; }; 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = "<group>"; }; 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = "<group>"; };
@ -548,6 +550,7 @@
5CADE79B292131E900072E13 /* ContactPreferencesView.swift */, 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */,
5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */, 5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */,
5CBE6C132944CC12002D9531 /* ScanCodeView.swift */, 5CBE6C132944CC12002D9531 /* ScanCodeView.swift */,
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */,
); );
path = Chat; path = Chat;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1058,6 +1061,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */,
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */, 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */,
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */, 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */,
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */, 5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */,

View File

@ -36,6 +36,7 @@ public enum ChatCommand {
case apiStorageEncryption(config: DBEncryptionConfig) case apiStorageEncryption(config: DBEncryptionConfig)
case apiGetChats(userId: Int64) case apiGetChats(userId: Int64)
case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String) 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 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 apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool)
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) 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 .apiGetChats(userId): return "/_get chats \(userId) pcc=on"
case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" + case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" +
(search == "" ? "" : " search=\(search)") (search == "" ? "" : " search=\(search)")
case let .apiGetChatItemInfo(itemId): return "/_get item info \(itemId)"
case let .apiSendMessage(type, id, file, quotedItemId, mc, live): case let .apiSendMessage(type, id, file, quotedItemId, mc, live):
let msg = encodeJSON(ComposedMessage(filePath: file, quotedItemId: quotedItemId, msgContent: mc)) let msg = encodeJSON(ComposedMessage(filePath: file, quotedItemId: quotedItemId, msgContent: mc))
return "/_send \(ref(type, id)) live=\(onOff(live)) json \(msg)" return "/_send \(ref(type, id)) live=\(onOff(live)) json \(msg)"
@ -247,6 +249,7 @@ public enum ChatCommand {
case .apiStorageEncryption: return "apiStorageEncryption" case .apiStorageEncryption: return "apiStorageEncryption"
case .apiGetChats: return "apiGetChats" case .apiGetChats: return "apiGetChats"
case .apiGetChat: return "apiGetChat" case .apiGetChat: return "apiGetChat"
case .apiGetChatItemInfo: return "apiGetChatItemInfo"
case .apiSendMessage: return "apiSendMessage" case .apiSendMessage: return "apiSendMessage"
case .apiUpdateChatItem: return "apiUpdateChatItem" case .apiUpdateChatItem: return "apiUpdateChatItem"
case .apiDeleteChatItem: return "apiDeleteChatItem" case .apiDeleteChatItem: return "apiDeleteChatItem"
@ -385,6 +388,7 @@ public enum ChatResponse: Decodable, Error {
case chatSuspended case chatSuspended
case apiChats(user: User, chats: [ChatData]) case apiChats(user: User, chats: [ChatData])
case apiChat(user: User, chat: ChatData) case apiChat(user: User, chat: ChatData)
case chatItemInfo(user: User, chatItem: AChatItem, chatItemInfo: ChatItemInfo)
case userProtoServers(user: User, servers: UserProtoServers) case userProtoServers(user: User, servers: UserProtoServers)
case serverTestResult(user: User, testServer: String, testFailure: ProtocolTestFailure?) case serverTestResult(user: User, testServer: String, testFailure: ProtocolTestFailure?)
case chatItemTTL(user: User, chatItemTTL: Int64?) case chatItemTTL(user: User, chatItemTTL: Int64?)
@ -501,6 +505,7 @@ public enum ChatResponse: Decodable, Error {
case .chatSuspended: return "chatSuspended" case .chatSuspended: return "chatSuspended"
case .apiChats: return "apiChats" case .apiChats: return "apiChats"
case .apiChat: return "apiChat" case .apiChat: return "apiChat"
case .chatItemInfo: return "chatItemInfo"
case .userProtoServers: return "userProtoServers" case .userProtoServers: return "userProtoServers"
case .serverTestResult: return "serverTestResult" case .serverTestResult: return "serverTestResult"
case .chatItemTTL: return "chatItemTTL" case .chatItemTTL: return "chatItemTTL"
@ -616,6 +621,7 @@ public enum ChatResponse: Decodable, Error {
case .chatSuspended: return noDetails case .chatSuspended: return noDetails
case let .apiChats(u, chats): return withUser(u, String(describing: chats)) case let .apiChats(u, chats): return withUser(u, String(describing: chats))
case let .apiChat(u, chat): return withUser(u, String(describing: chat)) 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 .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 .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))")
case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL)) case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL))

View File

@ -2857,3 +2857,18 @@ public enum ChatItemTTL: Hashable, Identifiable, Comparable {
return lhs.comparisonValue < rhs.comparisonValue 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
}

View File

@ -650,15 +650,18 @@ processChatCommand = \case
case cci of case cci of
CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive}, content = ciContent} -> do CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive}, content = ciContent} -> do
case (ciContent, itemSharedMsgId) of case (ciContent, itemSharedMsgId) of
(CISndMsgContent oldMC, Just itemSharedMId) -> do (CISndMsgContent oldMC, Just itemSharedMId) ->
(SndMessage {msgId}, _) <- sendDirectContactMessage ct (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) if mc /= oldMC
ci' <- withStore' $ \db -> do then do
currentTs <- liftIO getCurrentTime (SndMessage {msgId}, _) <- sendDirectContactMessage ct (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive))
addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) ci' <- withStore' $ \db -> do
updateDirectChatItem' db user contactId ci (CISndMsgContent mc) live $ Just msgId currentTs <- liftIO getCurrentTime
startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc)
setActive $ ActiveC c updateDirectChatItem' db user contactId ci (CISndMsgContent mc) live $ Just msgId
pure $ CRChatItemUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci') startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci'
setActive $ ActiveC c
pure $ CRChatItemUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci')
else throwChatError CEInvalidChatItemUpdate
_ -> throwChatError CEInvalidChatItemUpdate _ -> throwChatError CEInvalidChatItemUpdate
CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate
CTGroup -> do CTGroup -> do
@ -668,15 +671,18 @@ processChatCommand = \case
case cci of case cci of
CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive}, content = ciContent} -> do CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive}, content = ciContent} -> do
case (ciContent, itemSharedMsgId) of case (ciContent, itemSharedMsgId) of
(CISndMsgContent oldMC, Just itemSharedMId) -> do (CISndMsgContent oldMC, Just itemSharedMId) ->
SndMessage {msgId} <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) if mc /= oldMC
ci' <- withStore' $ \db -> do then do
currentTs <- liftIO getCurrentTime SndMessage {msgId} <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive))
addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) ci' <- withStore' $ \db -> do
updateGroupChatItem db user groupId ci (CISndMsgContent mc) live $ Just msgId currentTs <- liftIO getCurrentTime
startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc)
setActive $ ActiveG gName updateGroupChatItem db user groupId ci (CISndMsgContent mc) live $ Just msgId
pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci') startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci'
setActive $ ActiveG gName
pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci')
else throwChatError CEInvalidChatItemUpdate
_ -> throwChatError CEInvalidChatItemUpdate _ -> throwChatError CEInvalidChatItemUpdate
CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate
CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> pure $ chatCmdError (Just user) "not supported"
@ -3324,12 +3330,15 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
updateRcvChatItem = do updateRcvChatItem = do
cci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId cci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId
case cci of case cci of
CChatItem SMDRcv ci@ChatItem {content = CIRcvMsgContent oldMC} -> do CChatItem SMDRcv ci@ChatItem {content = CIRcvMsgContent oldMC} ->
ci' <- withStore' $ \db -> do if mc /= oldMC
addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc) then do
updateDirectChatItem' db user contactId ci content live $ Just msgId ci' <- withStore' $ \db -> do
toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc)
startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' 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" _ -> messageError "x.msg.update: contact attempted invalid message update"
messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> m () messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> m ()
@ -3393,8 +3402,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
updateRcvChatItem = do updateRcvChatItem = do
cci <- withStore $ \db -> getGroupChatItemBySharedMsgId db user groupId groupMemberId sharedMsgId cci <- withStore $ \db -> getGroupChatItemBySharedMsgId db user groupId groupMemberId sharedMsgId
case cci of case cci of
CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv m', content = CIRcvMsgContent oldMC} -> do CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv m', content = CIRcvMsgContent oldMC} ->
if sameMemberId memberId m' if sameMemberId memberId m' && mc /= oldMC
then do then do
ci' <- withStore' $ \db -> do ci' <- withStore' $ \db -> do
addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc) 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') toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci')
setActive $ ActiveG g setActive $ ActiveG g
startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' 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" _ -> messageError "x.msg.update: group member attempted invalid message update"
groupMessageDelete :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe MemberId -> RcvMessage -> m () groupMessageDelete :: GroupInfo -> GroupMember -> SharedMsgId -> Maybe MemberId -> RcvMessage -> m ()

View File

@ -1044,6 +1044,7 @@ testGroupLiveMessage =
bob <# "#team alice> [LIVE ended] hello there" bob <# "#team alice> [LIVE ended] hello there"
cath <# "#team alice> [LIVE ended] hello there" cath <# "#team alice> [LIVE ended] hello there"
-- empty live message is also sent instantly -- empty live message is also sent instantly
threadDelay 1000000
alice `send` "/live #team" alice `send` "/live #team"
msgItemId2 <- lastItemId alice msgItemId2 <- lastItemId alice
bob <#. "#team alice> [LIVE started]" bob <#. "#team alice> [LIVE started]"
@ -1058,13 +1059,13 @@ testGroupLiveMessage =
alice <## "message history:" alice <## "message history:"
alice .<## ": hello 2" alice .<## ": hello 2"
alice .<## ":" alice .<## ":"
-- bobItemId <- lastItemId bob bobItemId <- lastItemId bob
-- bob ##> ("/_get item info " <> bobItemId) bob ##> ("/_get item info " <> bobItemId)
-- bob <##. "sent at: " bob <##. "sent at: "
-- bob <##. "received at: " bob <##. "received at: "
-- bob <## "message history:" bob <## "message history:"
-- bob .<## ": hello 2" bob .<## ": hello 2"
-- bob .<## ":" bob .<## ":"
testUpdateGroupProfile :: HasCallStack => FilePath -> IO () testUpdateGroupProfile :: HasCallStack => FilePath -> IO ()
testUpdateGroupProfile = testUpdateGroupProfile =