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
}
}