diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 21ef80cf6..6a15a3965 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -756,6 +756,8 @@ final class Chat: ObservableObject, Identifiable { case let .group(groupInfo): let m = groupInfo.membership return m.memberActive && m.memberRole >= .member + case .local: + return true default: return false } } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 9f3e96888..c1d0a264b 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -365,6 +365,13 @@ func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: } } +func apiCreateChatItem(noteFolderId: Int64, file: CryptoFile?, msg: MsgContent) async -> ChatItem? { + let r = await chatSendCmd(.apiCreateChatItem(noteFolderId: noteFolderId, file: file, msg: msg)) + if case let .newChatItem(_, aChatItem) = r { return aChatItem.chatItem } + createChatItemErrorAlert(r) + return nil +} + private func sendMessageErrorAlert(_ r: ChatResponse) { logger.error("apiSendMessage error: \(String(describing: r))") AlertManager.shared.showAlertMsg( @@ -373,6 +380,14 @@ private func sendMessageErrorAlert(_ r: ChatResponse) { ) } +private func createChatItemErrorAlert(_ r: ChatResponse) { + logger.error("apiCreateChatItem error: \(String(describing: r))") + AlertManager.shared.showAlertMsg( + title: "Error creating message", + message: "Error: \(String(describing: r))" + ) +} + func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool = false) async throws -> ChatItem { let r = await chatSendCmd(.apiUpdateChatItem(type: type, id: id, itemId: itemId, msg: msg, live: live), bgDelay: msgDelay) if case let .chatItemUpdated(_, aChatItem) = r { return aChatItem.chatItem } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index b6a070278..c94ba3f83 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -52,7 +52,7 @@ struct CIFileView: View { private var itemInteractive: Bool { if let file = file { switch (file.fileStatus) { - case .sndStored: return false + case .sndStored: return file.fileProtocol == .local case .sndTransfer: return false case .sndComplete: return false case .sndCancelled: return false @@ -107,12 +107,18 @@ struct CIFileView: View { title: "Waiting for file", message: "File will be received when your contact is online, please wait or check later!" ) + case .local: () } case .rcvComplete: logger.debug("CIFileView fileAction - in .rcvComplete") if let fileSource = getLoadedFileSource(file) { saveCryptoFile(fileSource) } + case .sndStored: + logger.debug("CIFileView fileAction - in .sndStored") + if file.fileProtocol == .local, let fileSource = getLoadedFileSource(file) { + saveCryptoFile(fileSource) + } default: break } } @@ -125,11 +131,13 @@ struct CIFileView: View { switch file.fileProtocol { case .xftp: progressView() case .smp: fileIcon("doc.fill") + case .local: fileIcon("doc.fill") } case let .sndTransfer(sndProgress, sndTotal): switch file.fileProtocol { case .xftp: progressCircle(sndProgress, sndTotal) case .smp: progressView() + case .local: EmptyView() } case .sndComplete: fileIcon("doc.fill", innerIcon: "checkmark", innerIconSize: 10) case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index 2e20e56b7..c7e89fc5e 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -53,6 +53,7 @@ struct CIImageView: View { title: "Waiting for image", message: "Image will be received when your contact is online, please wait or check later!" ) + case .local: () } case .rcvTransfer: () // ? case .rcvComplete: () // ? @@ -90,6 +91,7 @@ struct CIImageView: View { switch file.fileProtocol { case .xftp: progressView() case .smp: EmptyView() + case .local: EmptyView() } case .sndTransfer: progressView() case .sndComplete: fileIcon("checkmark", 10, 13) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift index e0d2bed47..a824ddc49 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -83,6 +83,7 @@ struct CIVideoView: View { title: "Waiting for video", message: "Video will be received when your contact is online, please wait or check later!" ) + case .local: () } case .rcvTransfer: () // ? case .rcvComplete: () // ? @@ -107,7 +108,7 @@ struct CIVideoView: View { private func videoViewEncrypted(_ file: CIFile, _ defaultPreview: UIImage, _ duration: Int) -> some View { return ZStack(alignment: .topTrailing) { ZStack(alignment: .center) { - let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete + let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local) imageView(defaultPreview) .fullScreenCover(isPresented: $showFullScreenPlayer) { if let decrypted = urlDecrypted { @@ -143,7 +144,7 @@ struct CIVideoView: View { DispatchQueue.main.async { videoWidth = w } return ZStack(alignment: .topTrailing) { ZStack(alignment: .center) { - let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete + let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local) VideoPlayerView(player: player, url: url, showControls: false) .frame(width: w, height: w * preview.size.height / preview.size.width) .onChange(of: m.stopPreviousRecPlay) { playingUrl in @@ -254,11 +255,13 @@ struct CIVideoView: View { switch file.fileProtocol { case .xftp: progressView() case .smp: EmptyView() + case .local: EmptyView() } case let .sndTransfer(sndProgress, sndTotal): switch file.fileProtocol { case .xftp: progressCircle(sndProgress, sndTotal) case .smp: progressView() + case .local: EmptyView() } case .sndComplete: fileIcon("checkmark", 10, 13) case .sndCancelled: fileIcon("xmark", 10, 13) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 7b5dd40e9..1f0ab9ca4 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -9,6 +9,8 @@ import SwiftUI import SimpleXChat +let notesChatColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.21) +let notesChatColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.19) let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12) let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17) private let sentQuoteColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.11) diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift index 69cfcd2ca..8dd43cc01 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -53,7 +53,9 @@ struct ChatItemInfoView: View { } private var title: String { - ci.chatDir.sent + ci.localNote + ? NSLocalizedString("Saved message", comment: "message info title") + : ci.chatDir.sent ? NSLocalizedString("Sent message", comment: "message info title") : NSLocalizedString("Received message", comment: "message info title") } @@ -110,7 +112,11 @@ struct ChatItemInfoView: View { .bold() .padding(.bottom) - infoRow("Sent at", localTimestamp(meta.itemTs)) + if ci.localNote { + infoRow("Created at", localTimestamp(meta.itemTs)) + } else { + infoRow("Sent at", localTimestamp(meta.itemTs)) + } if !ci.chatDir.sent { infoRow("Received at", localTimestamp(meta.createdAt)) } @@ -350,7 +356,12 @@ struct ChatItemInfoView: View { private func itemInfoShareText() -> String { let meta = ci.meta var shareText: [String] = [String.localizedStringWithFormat(NSLocalizedString("# %@", comment: "copied message info title, # "), title), ""] - shareText += [String.localizedStringWithFormat(NSLocalizedString("Sent at: %@", comment: "copied message info"), localTimestamp(meta.itemTs))] + shareText += [String.localizedStringWithFormat( + ci.localNote + ? NSLocalizedString("Created at: %@", comment: "copied message info") + : NSLocalizedString("Sent at: %@", comment: "copied message info"), + localTimestamp(meta.itemTs)) + ] if !ci.chatDir.sent { shareText += [String.localizedStringWithFormat(NSLocalizedString("Received at: %@", comment: "copied message info"), localTimestamp(meta.createdAt))] } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index a09c5643b..af53e7e47 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -151,6 +151,8 @@ struct ChatView: View { ) ) } + } else if case .local = cInfo { + ChatInfoToolbar(chat: chat) } } ToolbarItem(placement: .navigationBarTrailing) { @@ -205,6 +207,8 @@ struct ChatView: View { Image(systemName: "ellipsis") } } + case .local: + searchButton() default: EmptyView() } @@ -636,7 +640,7 @@ struct ChatView: View { Button("Delete for me", role: .destructive) { deleteMessage(.cidmInternal) } - if let di = deletingItem, di.meta.editable { + if let di = deletingItem, di.meta.editable && !di.localNote { Button(broadcastDeleteButtonText, role: .destructive) { deleteMessage(.cidmBroadcast) } @@ -720,7 +724,7 @@ struct ChatView: View { } menu.append(rm) } - if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live { + if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live && !ci.localNote { menu.append(replyUIAction(ci)) } let fileSource = getLoadedFileSource(ci.file) @@ -748,9 +752,9 @@ struct ChatView: View { if revealed { menu.append(hideUIAction()) } - if ci.meta.itemDeleted == nil, + if ci.meta.itemDeleted == nil && !ci.localNote, let file = ci.file, - let cancelAction = file.cancelAction { + let cancelAction = file.cancelAction { menu.append(cancelFileUIAction(file.fileId, cancelAction)) } if !live || !ci.meta.isLive { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 1fd006d49..b59792609 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -295,7 +295,7 @@ struct ComposeView: View { sendMessage(ttl: ttl) resetLinkPreview() }, - sendLiveMessage: sendLiveMessage, + sendLiveMessage: chat.chatInfo.chatType != .local ? sendLiveMessage : nil, updateLiveMessage: updateLiveMessage, cancelLiveMessage: { composeState.liveMessage = nil @@ -792,15 +792,17 @@ struct ComposeView: View { } func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? { - if let chatItem = await apiSendMessage( - type: chat.chatInfo.chatType, - id: chat.chatInfo.apiId, - file: file, - quotedItemId: quoted, - msg: mc, - live: live, - ttl: ttl - ) { + if let chatItem = chat.chatInfo.chatType == .local + ? await apiCreateChatItem(noteFolderId: chat.chatInfo.apiId, file: file, msg: mc) + : await apiSendMessage( + type: chat.chatInfo.chatType, + id: chat.chatInfo.apiId, + file: file, + quotedItemId: quoted, + msg: mc, + live: live, + ttl: ttl + ) { await MainActor.run { chatModel.removeLiveDummy(animated: false) chatModel.addChatItem(chat.chatInfo, chatItem) diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 18464b3bb..7fbc1e4ac 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -44,6 +44,8 @@ struct ChatListNavLink: View { contactNavLink(contact) case let .group(groupInfo): groupNavLink(groupInfo) + case let .local(noteFolder): + noteFolderNavLink(noteFolder) case let .contactRequest(cReq): contactRequestNavLink(cReq) case let .contactConnection(cConn): @@ -195,6 +197,24 @@ struct ChatListNavLink: View { } } + @ViewBuilder private func noteFolderNavLink(_ noteFolder: NoteFolder) -> some View { + NavLinkPlain( + tag: chat.chatInfo.id, + selection: $chatModel.chatId, + label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }, + disabled: !noteFolder.ready + ) + .frame(height: rowHeights[dynamicTypeSize]) + .swipeActions(edge: .leading, allowsFullSwipe: true) { + markReadButton() + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + if !chat.chatItems.isEmpty { + clearNoteFolderButton() + } + } + } + private func joinGroupButton() -> some View { Button { inProgress = true @@ -253,6 +273,15 @@ struct ChatListNavLink: View { .tint(Color.orange) } + private func clearNoteFolderButton() -> some View { + Button { + AlertManager.shared.showAlert(clearNoteFolderAlert()) + } label: { + Label("Clear", systemImage: "gobackward") + } + .tint(Color.orange) + } + private func leaveGroupChatButton(_ groupInfo: GroupInfo) -> some View { Button { AlertManager.shared.showAlert(leaveGroupAlert(groupInfo)) @@ -357,6 +386,17 @@ struct ChatListNavLink: View { ) } + private func clearNoteFolderAlert() -> Alert { + Alert( + title: Text("Clear private notes?"), + message: Text("All messages will be deleted - this cannot be undone!"), + primaryButton: .destructive(Text("Clear")) { + Task { await clearChat(chat) } + }, + secondaryButton: .cancel() + ) + } + private func leaveGroupAlert(_ groupInfo: GroupInfo) -> Alert { Alert( title: Text("Leave group?"), diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 1b4531b08..22807f618 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -247,6 +247,8 @@ struct ChatListView: View { return s == "" ? (filtered(chat) || gInfo.membership.memberStatus == .memInvited) : viewNameContains(cInfo, s) + case .local: + return s == "" || viewNameContains(cInfo, s) case .contactRequest: return s == "" || viewNameContains(cInfo, s) case let .contactConnection(conn): diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 0a53e0511..4f57158af 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -134,9 +134,9 @@ struct ChatPreviewView: View { .foregroundColor(.white) .padding(.horizontal, 4) .frame(minWidth: 18, minHeight: 18) - .background(chat.chatInfo.ntfsEnabled ? Color.accentColor : Color.secondary) + .background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? Color.accentColor : Color.secondary) .cornerRadius(10) - } else if !chat.chatInfo.ntfsEnabled { + } else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local { Image(systemName: "speaker.slash.fill") .foregroundColor(.secondary) } else if chat.chatInfo.chatSettings?.favorite ?? false { diff --git a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift index 1b344148c..e253cdd72 100644 --- a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift +++ b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift @@ -10,6 +10,7 @@ import SwiftUI import SimpleXChat struct ChatInfoImage: View { + @Environment(\.colorScheme) var colorScheme @ObservedObject var chat: Chat var color = Color(uiColor: .tertiarySystemGroupedBackground) @@ -18,13 +19,16 @@ struct ChatInfoImage: View { switch chat.chatInfo { case .direct: iconName = "person.crop.circle.fill" case .group: iconName = "person.2.circle.fill" + case .local: iconName = "folder.circle.fill" case .contactRequest: iconName = "person.crop.circle.fill" default: iconName = "circle.fill" } + let notesColor = colorScheme == .light ? notesChatColorLight : notesChatColorDark + let iconColor = if case .local = chat.chatInfo { notesColor } else { color } return ProfileImage( imageStr: chat.chatInfo.image, iconName: iconName, - color: color + color: iconColor ) } } diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index c6b149bf7..61c439fb3 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -783,6 +783,8 @@ func autoReceiveFile(_ file: CIFile) -> ChatItem? { case .xftp: apiSetFileToReceive(fileId: file.fileId, encrypted: encrypted) return nil + case .local: + return nil } } diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 1a8f935f2..aff0d0dc3 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -41,6 +41,7 @@ public enum ChatCommand { case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String) case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) case apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool, ttl: Int?) + case apiCreateChatItem(noteFolderId: Int64, file: CryptoFile?, msg: MsgContent) case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool) case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64) @@ -178,6 +179,9 @@ public enum ChatCommand { let msg = encodeJSON(ComposedMessage(fileSource: file, quotedItemId: quotedItemId, msgContent: mc)) let ttlStr = ttl != nil ? "\(ttl!)" : "default" return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msg)" + case let .apiCreateChatItem(noteFolderId, file, mc): + let msg = encodeJSON(ComposedMessage(fileSource: file, msgContent: mc)) + return "/_create *\(noteFolderId) json \(msg)" case let .apiUpdateChatItem(type, id, itemId, mc, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(mc.cmdString)" case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)" case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)" @@ -315,6 +319,7 @@ public enum ChatCommand { case .apiGetChat: return "apiGetChat" case .apiGetChatItemInfo: return "apiGetChatItemInfo" case .apiSendMessage: return "apiSendMessage" + case .apiCreateChatItem: return "apiCreateChatItem" case .apiUpdateChatItem: return "apiUpdateChatItem" case .apiDeleteChatItem: return "apiDeleteChatItem" case .apiConnectContactViaAddress: return "apiConnectContactViaAddress" diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 137642fce..5426596a4 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -180,6 +180,7 @@ public struct UserProfileUpdateSummary: Decodable { public enum ChatType: String { case direct = "@" case group = "#" + case local = "*" case contactRequest = "<@" case contactConnection = ":" } @@ -1095,17 +1096,21 @@ public enum GroupFeatureEnabled: String, Codable, Identifiable { public enum ChatInfo: Identifiable, Decodable, NamedChat { case direct(contact: Contact) case group(groupInfo: GroupInfo) + case local(noteFolder: NoteFolder) case contactRequest(contactRequest: UserContactRequest) case contactConnection(contactConnection: PendingContactConnection) case invalidJSON(json: String) private static let invalidChatName = NSLocalizedString("invalid chat", comment: "invalid chat data") + static let privateNotesChatName = NSLocalizedString("Private notes", comment: "name of notes to self") + public var localDisplayName: String { get { switch self { case let .direct(contact): return contact.localDisplayName case let .group(groupInfo): return groupInfo.localDisplayName + case .local: return "" case let .contactRequest(contactRequest): return contactRequest.localDisplayName case let .contactConnection(contactConnection): return contactConnection.localDisplayName case .invalidJSON: return ChatInfo.invalidChatName @@ -1118,6 +1123,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.displayName case let .group(groupInfo): return groupInfo.displayName + case .local: return ChatInfo.privateNotesChatName case let .contactRequest(contactRequest): return contactRequest.displayName case let .contactConnection(contactConnection): return contactConnection.displayName case .invalidJSON: return ChatInfo.invalidChatName @@ -1130,6 +1136,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.fullName case let .group(groupInfo): return groupInfo.fullName + case .local: return "" case let .contactRequest(contactRequest): return contactRequest.fullName case let .contactConnection(contactConnection): return contactConnection.fullName case .invalidJSON: return ChatInfo.invalidChatName @@ -1142,6 +1149,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.image case let .group(groupInfo): return groupInfo.image + case .local: return nil case let .contactRequest(contactRequest): return contactRequest.image case let .contactConnection(contactConnection): return contactConnection.image case .invalidJSON: return nil @@ -1154,6 +1162,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.localAlias case let .group(groupInfo): return groupInfo.localAlias + case .local: return "" case let .contactRequest(contactRequest): return contactRequest.localAlias case let .contactConnection(contactConnection): return contactConnection.localAlias case .invalidJSON: return "" @@ -1166,6 +1175,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.id case let .group(groupInfo): return groupInfo.id + case let .local(noteFolder): return noteFolder.id case let .contactRequest(contactRequest): return contactRequest.id case let .contactConnection(contactConnection): return contactConnection.id case .invalidJSON: return "" @@ -1178,6 +1188,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case .direct: return .direct case .group: return .group + case .local: return .local case .contactRequest: return .contactRequest case .contactConnection: return .contactConnection case .invalidJSON: return .direct @@ -1190,6 +1201,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.apiId case let .group(groupInfo): return groupInfo.apiId + case let .local(noteFolder): return noteFolder.apiId case let .contactRequest(contactRequest): return contactRequest.apiId case let .contactConnection(contactConnection): return contactConnection.apiId case .invalidJSON: return 0 @@ -1202,6 +1214,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.ready case let .group(groupInfo): return groupInfo.ready + case let .local(noteFolder): return noteFolder.ready case let .contactRequest(contactRequest): return contactRequest.ready case let .contactConnection(contactConnection): return contactConnection.ready case .invalidJSON: return false @@ -1214,6 +1227,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.sendMsgEnabled case let .group(groupInfo): return groupInfo.sendMsgEnabled + case let .local(noteFolder): return noteFolder.sendMsgEnabled case let .contactRequest(contactRequest): return contactRequest.sendMsgEnabled case let .contactConnection(contactConnection): return contactConnection.sendMsgEnabled case .invalidJSON: return false @@ -1226,6 +1240,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.contactConnIncognito case let .group(groupInfo): return groupInfo.membership.memberIncognito + case .local: return false case .contactRequest: return false case let .contactConnection(contactConnection): return contactConnection.incognito case .invalidJSON: return false @@ -1268,6 +1283,11 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case .voice: return prefs.voice.on case .calls: return false } + case .local: + switch feature { + case .voice: return true + default: return false + } default: return false } } @@ -1329,6 +1349,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.createdAt case let .group(groupInfo): return groupInfo.createdAt + case let .local(noteFolder): return noteFolder.createdAt case let .contactRequest(contactRequest): return contactRequest.createdAt case let .contactConnection(contactConnection): return contactConnection.createdAt case .invalidJSON: return .now @@ -1339,6 +1360,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { switch self { case let .direct(contact): return contact.updatedAt case let .group(groupInfo): return groupInfo.updatedAt + case let .local(noteFolder): return noteFolder.updatedAt case let .contactRequest(contactRequest): return contactRequest.updatedAt case let .contactConnection(contactConnection): return contactConnection.updatedAt case .invalidJSON: return .now @@ -1348,6 +1370,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { public struct SampleData { public var direct: ChatInfo public var group: ChatInfo + public var local: ChatInfo public var contactRequest: ChatInfo public var contactConnection: ChatInfo } @@ -1355,6 +1378,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { public static var sampleData: ChatInfo.SampleData = SampleData( direct: ChatInfo.direct(contact: Contact.sampleData), group: ChatInfo.group(groupInfo: GroupInfo.sampleData), + local: ChatInfo.local(noteFolder: NoteFolder.sampleData), contactRequest: ChatInfo.contactRequest(contactRequest: UserContactRequest.sampleData), contactConnection: ChatInfo.contactConnection(contactConnection: PendingContactConnection.getSampleData()) ) @@ -2010,6 +2034,37 @@ public enum GroupMemberStatus: String, Decodable { } } +public struct NoteFolder: Identifiable, Decodable, NamedChat { + public var noteFolderId: Int64 + public var favorite: Bool + public var unread: Bool + var createdAt: Date + public var updatedAt: Date + + public var id: ChatId { get { "*\(noteFolderId)" } } + public var apiId: Int64 { get { noteFolderId } } + public var ready: Bool { get { true } } + public var sendMsgEnabled: Bool { get { true } } + public var displayName: String { get { ChatInfo.privateNotesChatName } } + public var fullName: String { get { "" } } + public var image: String? { get { nil } } + public var localAlias: String { get { "" } } + + public var canEdit: Bool { true } + + public var canDelete: Bool { true } + + public var canAddMembers: Bool { false } + + public static let sampleData = NoteFolder( + noteFolderId: 1, + favorite: false, + unread: false, + createdAt: .now, + updatedAt: .now + ) +} + public enum InvitedBy: Decodable { case contact(byContactId: Int64) case user @@ -2262,6 +2317,13 @@ public struct ChatItem: Identifiable, Decodable { } } + public var localNote: Bool { + switch chatDir { + case .localSnd, .localRcv: return true + default: return false + } + } + public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember)? { switch (chatInfo, chatDir) { case let (.group(groupInfo), .groupRcv(groupMember)): @@ -2423,6 +2485,8 @@ public enum CIDirection: Decodable { case directRcv case groupSnd case groupRcv(groupMember: GroupMember) + case localSnd + case localRcv public var sent: Bool { get { @@ -2431,6 +2495,8 @@ public enum CIDirection: Decodable { case .directRcv: return false case .groupSnd: return true case .groupRcv: return false + case .localSnd: return true + case .localRcv: return false } } } @@ -2756,6 +2822,8 @@ public struct CIQuote: Decodable, ItemContent { case .directRcv: return nil case .groupSnd: return membership?.displayName ?? "you" case let .groupRcv(member): return member.displayName + case .localSnd: return "you" + case .localRcv: return nil case nil: return nil } } @@ -2996,6 +3064,7 @@ private var rcvCancelAction = CancelAction( public enum FileProtocol: String, Decodable { case smp = "smp" case xftp = "xftp" + case local = "local" } public enum CIFileStatus: Decodable, Equatable { diff --git a/apps/ios/SimpleXChat/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift index 748a7841d..7496bf721 100644 --- a/apps/ios/SimpleXChat/FileUtils.swift +++ b/apps/ios/SimpleXChat/FileUtils.swift @@ -22,6 +22,8 @@ public let MAX_VIDEO_SIZE_AUTO_RCV: Int64 = 1_047_552 // 1023KB public let MAX_FILE_SIZE_XFTP: Int64 = 1_073_741_824 // 1GB +public let MAX_FILE_SIZE_LOCAL: Int64 = Int64.max + public let MAX_FILE_SIZE_SMP: Int64 = 8000000 public let MAX_VOICE_MESSAGE_LENGTH = TimeInterval(300) @@ -240,6 +242,7 @@ public func getMaxFileSize(_ fileProtocol: FileProtocol) -> Int64 { switch fileProtocol { case .xftp: return MAX_FILE_SIZE_XFTP case .smp: return MAX_FILE_SIZE_SMP + case .local: return MAX_FILE_SIZE_LOCAL } }