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:
parent
0b8d9d11e2
commit
63f344bde6
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
147
apps/ios/Shared/Views/Chat/ChatItemInfoView.swift
Normal file
147
apps/ios/Shared/Views/Chat/ChatItemInfoView.swift
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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 */,
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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 ()
|
||||||
|
@ -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 =
|
||||||
|
Loading…
Reference in New Issue
Block a user