mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-24 23:55:50 +00:00
wip
This commit is contained in:
@@ -42,10 +42,12 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case apiGetSettings(settings: AppSettings)
|
||||
case apiGetChatTags(userId: Int64)
|
||||
case apiGetChats(userId: Int64)
|
||||
case apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag?, pagination: ChatPagination, search: String)
|
||||
case apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag?, pagination: ChatPagination, search: String, parentItemId: Int64? = nil)
|
||||
case apiGetChatContentTypes(chatId: ChatId, scope: GroupChatScope?)
|
||||
case apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64)
|
||||
case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, sendAsGroup: Bool, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
|
||||
case apiSendComment(groupId: Int64, parentItemId: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
|
||||
case apiSetCommentsDisabled(groupId: Int64, parentItemId: Int64, disabled: Bool)
|
||||
case apiCreateChatTag(tag: ChatTagData)
|
||||
case apiSetChatTags(type: ChatType, id: Int64, tagIds: [Int64])
|
||||
case apiDeleteChatTag(tagId: Int64)
|
||||
@@ -231,9 +233,10 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case let .apiGetSettings(settings): return "/_get app settings \(encodeJSON(settings))"
|
||||
case let .apiGetChatTags(userId): return "/_get tags \(userId)"
|
||||
case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on"
|
||||
case let .apiGetChat(chatId, scope, contentTag, pagination, search):
|
||||
case let .apiGetChat(chatId, scope, contentTag, pagination, search, parentItemId):
|
||||
let parent = parentItemId != nil ? " parent=\(parentItemId!)" : ""
|
||||
let tag = contentTag != nil ? " content=\(contentTag!.rawValue)" : ""
|
||||
return "/_get chat \(chatId)\(scopeRef(scope))\(tag) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)")
|
||||
return "/_get chat \(chatId)\(scopeRef(scope))\(parent)\(tag) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)")
|
||||
case let .apiGetChatContentTypes(chatId, scope): return "/_get content types \(chatId)\(scopeRef(scope))"
|
||||
case let .apiGetChatItemInfo(type, id, scope, itemId): return "/_get item info \(ref(type, id, scope: scope)) \(itemId)"
|
||||
case let .apiSendMessages(type, id, scope, sendAsGroup, live, ttl, composedMessages):
|
||||
@@ -241,6 +244,12 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
|
||||
let asGroup = sendAsGroup ? "(as_group=on)" : ""
|
||||
return "/_send \(ref(type, id, scope: scope))\(asGroup) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
|
||||
case let .apiSendComment(groupId, parentItemId, live, ttl, composedMessages):
|
||||
let msgs = encodeJSON(composedMessages)
|
||||
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
|
||||
return "/_comment #\(groupId) \(parentItemId) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
|
||||
case let .apiSetCommentsDisabled(groupId, parentItemId, disabled):
|
||||
return "/_comments_disabled #\(groupId) \(parentItemId) \(onOff(disabled))"
|
||||
case let .apiCreateChatTag(tag): return "/_create tag \(encodeJSON(tag))"
|
||||
case let .apiSetChatTags(type, id, tagIds): return "/_tags \(ref(type, id, scope: nil)) \(tagIds.map({ "\($0)" }).joined(separator: ","))"
|
||||
case let .apiDeleteChatTag(tagId): return "/_delete tag \(tagId)"
|
||||
@@ -442,6 +451,8 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case .apiGetChatContentTypes: return "apiGetChatContentTypes"
|
||||
case .apiGetChatItemInfo: return "apiGetChatItemInfo"
|
||||
case .apiSendMessages: return "apiSendMessages"
|
||||
case .apiSendComment: return "apiSendComment"
|
||||
case .apiSetCommentsDisabled: return "apiSetCommentsDisabled"
|
||||
case .apiCreateChatTag: return "apiCreateChatTag"
|
||||
case .apiSetChatTags: return "apiSetChatTags"
|
||||
case .apiDeleteChatTag: return "apiDeleteChatTag"
|
||||
|
||||
@@ -58,6 +58,10 @@ private func addTermItem(_ items: inout [TerminalItem], _ item: TerminalItem) {
|
||||
enum SecondaryItemsModelFilter {
|
||||
case groupChatScopeContext(groupScopeInfo: GroupChatScopeInfo)
|
||||
case msgContentTagContext(contentTag: MsgContentTag)
|
||||
// Comments thread under a channel post. The parent ChatItem carries the
|
||||
// toolbar preview text (slice 4) and the parent.id used to route inbound
|
||||
// items (ci.parentChatItemId == parent.id).
|
||||
case groupChannelMsgContext(parent: ChatItem)
|
||||
|
||||
func descr() -> String {
|
||||
switch self {
|
||||
@@ -65,6 +69,8 @@ enum SecondaryItemsModelFilter {
|
||||
return "groupChatScopeContext \(groupScopeInfo.toChatScope())"
|
||||
case let .msgContentTagContext(contentTag):
|
||||
return "msgContentTagContext \(contentTag.rawValue)"
|
||||
case let .groupChannelMsgContext(parent):
|
||||
return "groupChannelMsgContext parent=\(parent.id)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,11 +114,58 @@ class ItemsModel: ObservableObject {
|
||||
|
||||
// Spec: spec/state.md#loadSecondaryChat
|
||||
static func loadSecondaryChat(_ chatId: ChatId, chatFilter: SecondaryItemsModelFilter, willNavigate: @escaping () -> Void = {}) {
|
||||
// Comments-thread context: fetch with parentItemId, then inject the local-only
|
||||
// ChannelMsgInfo carrier into the returned ChatInfo (the wire shape has no
|
||||
// channelMsgInfo field; the carrier is set here so views can read it from cInfo).
|
||||
// The owner-side parent may briefly lack itemSharedMsgId during send — guard against that.
|
||||
if case let .groupChannelMsgContext(parent) = chatFilter {
|
||||
guard let sharedId = parent.meta.itemSharedMsgId else { return }
|
||||
let im = ItemsModel(secondaryIMFilter: chatFilter)
|
||||
ChatModel.shared.secondaryIM = im
|
||||
Task {
|
||||
do {
|
||||
let (chat, _) = try await apiGetChat(
|
||||
chatId: chatId,
|
||||
scope: nil,
|
||||
pagination: .last(count: loadItemsPerPage),
|
||||
parentItemId: parent.id
|
||||
)
|
||||
let rewritten = injectChannelMsgInfo(chat.chatInfo, parent: parent, sharedId: sharedId)
|
||||
await MainActor.run {
|
||||
im.reversedChatItems = chat.chatItems.reversed()
|
||||
ChatModel.shared.chatId = chatId
|
||||
// Replace the chat in the model with the rewritten info so views
|
||||
// querying secondaryIM.cInfo see the injected ChannelMsgInfo.
|
||||
if let i = ChatModel.shared.getChatIndex(chatId) {
|
||||
ChatModel.shared.chats[i].chatInfo = rewritten
|
||||
}
|
||||
willNavigate()
|
||||
}
|
||||
} catch {
|
||||
logger.error("loadSecondaryChat (comments thread) failed: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
let im = ItemsModel(secondaryIMFilter: chatFilter)
|
||||
ChatModel.shared.secondaryIM = im
|
||||
im.loadOpenChat(chatId, willNavigate: willNavigate)
|
||||
}
|
||||
|
||||
// Rewrites a ChatInfo.group's third associated value to embed the comments-thread
|
||||
// carrier. Other ChatInfo cases (direct, local, contactRequest, contactConnection)
|
||||
// are not comment contexts and pass through unchanged.
|
||||
private static func injectChannelMsgInfo(_ cInfo: ChatInfo, parent: ChatItem, sharedId: String) -> ChatInfo {
|
||||
if case let .group(groupInfo, groupChatScope, _) = cInfo {
|
||||
return .group(
|
||||
groupInfo: groupInfo,
|
||||
groupChatScope: groupChatScope,
|
||||
channelMsgInfo: ChannelMsgInfo(channelMsgItem: parent, channelMsgSharedId: sharedId)
|
||||
)
|
||||
}
|
||||
return cInfo
|
||||
}
|
||||
|
||||
// Spec: spec/state.md#loadOpenChat
|
||||
func loadOpenChat(_ chatId: ChatId, willNavigate: @escaping () -> Void = {}) {
|
||||
navigationTimeoutTask?.cancel()
|
||||
@@ -511,7 +564,7 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
func getGroupChat(_ groupId: Int64) -> Chat? {
|
||||
chats.first { chat in
|
||||
if case let .group(groupInfo, _) = chat.chatInfo {
|
||||
if case let .group(groupInfo, _, _) = chat.chatInfo {
|
||||
return groupInfo.groupId == groupId
|
||||
} else {
|
||||
return false
|
||||
@@ -566,8 +619,8 @@ final class ChatModel: ObservableObject {
|
||||
// Spec: spec/state.md#updateChatInfo
|
||||
func updateChatInfo(_ cInfo: ChatInfo) {
|
||||
if let i = getChatIndex(cInfo.id) {
|
||||
if case let .group(groupInfo, groupChatScope) = cInfo, groupChatScope != nil {
|
||||
chats[i].chatInfo = .group(groupInfo: groupInfo, groupChatScope: nil)
|
||||
if case let .group(groupInfo, groupChatScope, _) = cInfo, groupChatScope != nil {
|
||||
chats[i].chatInfo = .group(groupInfo: groupInfo, groupChatScope: nil, channelMsgInfo: nil)
|
||||
} else {
|
||||
chats[i].chatInfo = cInfo
|
||||
}
|
||||
@@ -592,7 +645,7 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
func updateGroup(_ groupInfo: GroupInfo) {
|
||||
updateChat(.group(groupInfo: groupInfo, groupChatScope: nil))
|
||||
updateChat(.group(groupInfo: groupInfo, groupChatScope: nil, channelMsgInfo: nil))
|
||||
}
|
||||
|
||||
private func updateChat(_ cInfo: ChatInfo, addMissing: Bool = true) {
|
||||
@@ -657,7 +710,7 @@ final class ChatModel: ObservableObject {
|
||||
// update chat list
|
||||
if let i = getChatIndex(cInfo.id) {
|
||||
// update preview
|
||||
if cInfo.groupChatScope() == nil || cInfo.groupInfo?.membership.memberPending ?? false {
|
||||
if (cInfo.groupChatScope() == nil && cInfo.channelMsgInfo() == nil) || cInfo.groupInfo?.membership.memberPending ?? false {
|
||||
chats[i].chatItems = switch cInfo {
|
||||
case .group:
|
||||
if let currentPreviewItem = chats[i].chatItems.first {
|
||||
@@ -679,7 +732,7 @@ final class ChatModel: ObservableObject {
|
||||
// pop chat
|
||||
popChatCollector.throttlePopChat(cInfo.id, currentPosition: i)
|
||||
} else {
|
||||
if cInfo.groupChatScope() == nil {
|
||||
if cInfo.groupChatScope() == nil && cInfo.channelMsgInfo() == nil {
|
||||
addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
|
||||
} else {
|
||||
addChat(Chat(chatInfo: cInfo, chatItems: []))
|
||||
@@ -709,6 +762,15 @@ final class ChatModel: ObservableObject {
|
||||
default:
|
||||
nil
|
||||
}
|
||||
} else if cInfo.channelMsgInfo() != nil {
|
||||
// Comments thread open. Inbound items lack channelMsgInfo on the wire (the
|
||||
// carrier is local-only), so route by ci.parentChatItemId == parent.id.
|
||||
switch secondaryIM?.secondaryIMFilter {
|
||||
case let .some(.groupChannelMsgContext(parent)):
|
||||
(cInfo.id == chatId && ci.parentChatItemId == parent.id) ? secondaryIM : nil
|
||||
default:
|
||||
nil
|
||||
}
|
||||
} else {
|
||||
cInfo.id == chatId ? im : nil
|
||||
}
|
||||
@@ -717,7 +779,7 @@ final class ChatModel: ObservableObject {
|
||||
func upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
||||
// update chat list
|
||||
var itemAdded: Bool = false
|
||||
if cInfo.groupChatScope() == nil {
|
||||
if cInfo.groupChatScope() == nil && cInfo.channelMsgInfo() == nil {
|
||||
if let chat = getChat(cInfo.id) {
|
||||
if let pItem = chat.chatItems.last {
|
||||
if pItem.id == cItem.id || (chatId == cInfo.id && im.reversedChatItems.first(where: { $0.id == cItem.id }) == nil) {
|
||||
@@ -788,7 +850,7 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
// update chat list
|
||||
if cInfo.groupChatScope() == nil {
|
||||
if cInfo.groupChatScope() == nil && cInfo.channelMsgInfo() == nil {
|
||||
if cItem.isRcvNew {
|
||||
unreadCollector.changeUnreadCounter(cInfo.id, by: -1, unreadMentions: cItem.meta.userMention ? -1 : 0)
|
||||
}
|
||||
@@ -1290,7 +1352,7 @@ final class ChatModel: ObservableObject {
|
||||
func removeWallpaperFilesFromChat(_ chat: Chat) {
|
||||
if case let .direct(contact) = chat.chatInfo {
|
||||
removeWallpaperFilesFromTheme(contact.uiThemes)
|
||||
} else if case let .group(groupInfo, _) = chat.chatInfo {
|
||||
} else if case let .group(groupInfo, _, _) = chat.chatInfo {
|
||||
removeWallpaperFilesFromTheme(groupInfo.uiThemes)
|
||||
}
|
||||
}
|
||||
@@ -1356,7 +1418,7 @@ final class Chat: ObservableObject, Identifiable, ChatLike {
|
||||
|
||||
var supportUnreadCount: Int {
|
||||
switch chatInfo {
|
||||
case let .group(groupInfo, _):
|
||||
case let .group(groupInfo, _, _):
|
||||
if groupInfo.canModerate {
|
||||
return groupInfo.membersRequireAttention
|
||||
} else {
|
||||
|
||||
@@ -450,8 +450,8 @@ func apiGetChatTagsAsync() async throws -> [ChatTag] {
|
||||
|
||||
let loadItemsPerPage = 50
|
||||
|
||||
func apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag? = nil, pagination: ChatPagination, search: String = "") async throws -> (Chat, NavigationInfo) {
|
||||
let r: ChatResponse0 = try await chatSendCmd(.apiGetChat(chatId: chatId, scope: scope, contentTag: contentTag, pagination: pagination, search: search))
|
||||
func apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag? = nil, pagination: ChatPagination, search: String = "", parentItemId: Int64? = nil) async throws -> (Chat, NavigationInfo) {
|
||||
let r: ChatResponse0 = try await chatSendCmd(.apiGetChat(chatId: chatId, scope: scope, contentTag: contentTag, pagination: pagination, search: search, parentItemId: parentItemId))
|
||||
if case let .apiChat(_, chat, navInfo) = r { return (Chat.init(chat), navInfo ?? NavigationInfo()) }
|
||||
throw r.unexpected
|
||||
}
|
||||
@@ -547,6 +547,21 @@ func apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, sendAsGr
|
||||
return await processSendMessageCmd(toChatType: type, cmd: cmd)
|
||||
}
|
||||
|
||||
// Send a comment on a channel post. The target chat is always a channel (group with useRelays);
|
||||
// the recipient is the relay, which forwards the comment to other subscribers.
|
||||
// Returns the locally-created ChatItem(s) for the sender's view; typically a singleton.
|
||||
func apiSendComment(groupId: Int64, parentItemId: Int64, live: Bool = false, ttl: Int? = nil, composedMessages: [ComposedMessage]) async -> [ChatItem]? {
|
||||
let cmd: ChatCommand = .apiSendComment(groupId: groupId, parentItemId: parentItemId, live: live, ttl: ttl, composedMessages: composedMessages)
|
||||
return await processSendMessageCmd(toChatType: .group, cmd: cmd)
|
||||
}
|
||||
|
||||
// Toggle commentsDisabled on a channel post via XMsgUpdate.prefs. The Haskell handler
|
||||
// returns CRCmdOk; the local parent ChatItem update arrives via the owner's own echo
|
||||
// of XMsgUpdate.prefs through the standard receive pipeline. No item returned here.
|
||||
func apiSetCommentsDisabled(groupId: Int64, parentItemId: Int64, disabled: Bool) async throws {
|
||||
try await sendCommandOkResp(.apiSetCommentsDisabled(groupId: groupId, parentItemId: parentItemId, disabled: disabled))
|
||||
}
|
||||
|
||||
private func processSendMessageCmd(toChatType: ChatType, cmd: ChatCommand) async -> [ChatItem]? {
|
||||
let chatModel = ChatModel.shared
|
||||
let r: APIResult<ChatResponse1>
|
||||
@@ -2919,7 +2934,7 @@ func groupChatItemsDeleted(_ user: UserRef, _ groupInfo: GroupInfo, _ chatItemID
|
||||
return
|
||||
}
|
||||
let im = ItemsModel.shared
|
||||
let cInfo = ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil)
|
||||
let cInfo = ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil, channelMsgInfo: nil)
|
||||
await MainActor.run {
|
||||
m.decreaseGroupReportsCounter(cInfo.id, by: chatItemIDs.count)
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ struct ChatInfoToolbar: View {
|
||||
}
|
||||
|
||||
private var channelSubscriberCount: Int64? {
|
||||
if case let .group(groupInfo, _) = chat.chatInfo,
|
||||
if case let .group(groupInfo, _, _) = chat.chatInfo,
|
||||
groupInfo.useRelays,
|
||||
let count = groupInfo.groupSummary.publicMemberCount,
|
||||
count > 0 {
|
||||
|
||||
@@ -956,7 +956,7 @@ struct ChatWallpaperEditorSheet: View {
|
||||
self.chat = chat
|
||||
self.themes = if case let ChatInfo.direct(contact) = chat.chatInfo, let uiThemes = contact.uiThemes {
|
||||
uiThemes
|
||||
} else if case let ChatInfo.group(groupInfo, _) = chat.chatInfo, let uiThemes = groupInfo.uiThemes {
|
||||
} else if case let ChatInfo.group(groupInfo, _, _) = chat.chatInfo, let uiThemes = groupInfo.uiThemes {
|
||||
uiThemes
|
||||
} else {
|
||||
ThemeModeOverrides()
|
||||
@@ -992,7 +992,7 @@ struct ChatWallpaperEditorSheet: View {
|
||||
private func themesFromChat(_ chat: Chat) -> ThemeModeOverrides {
|
||||
if case let ChatInfo.direct(contact) = chat.chatInfo, let uiThemes = contact.uiThemes {
|
||||
uiThemes
|
||||
} else if case let ChatInfo.group(groupInfo, _) = chat.chatInfo, let uiThemes = groupInfo.uiThemes {
|
||||
} else if case let ChatInfo.group(groupInfo, _, _) = chat.chatInfo, let uiThemes = groupInfo.uiThemes {
|
||||
uiThemes
|
||||
} else {
|
||||
ThemeModeOverrides()
|
||||
@@ -1070,12 +1070,12 @@ struct ChatWallpaperEditorSheet: View {
|
||||
chat.wrappedValue = Chat.init(chatInfo: ChatInfo.direct(contact: contact))
|
||||
themes = themesFromChat(chat.wrappedValue)
|
||||
}
|
||||
} else if case var ChatInfo.group(groupInfo, _) = chat.wrappedValue.chatInfo {
|
||||
} else if case var ChatInfo.group(groupInfo, _, _) = chat.wrappedValue.chatInfo {
|
||||
groupInfo.uiThemes = changedThemesConstant
|
||||
|
||||
await MainActor.run {
|
||||
ChatModel.shared.updateChatInfo(ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil))
|
||||
chat.wrappedValue = Chat.init(chatInfo: ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil))
|
||||
ChatModel.shared.updateChatInfo(ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil, channelMsgInfo: nil))
|
||||
chat.wrappedValue = Chat.init(chatInfo: ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil, channelMsgInfo: nil))
|
||||
themes = themesFromChat(chat.wrappedValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ struct CIRcvDecryptionError: View {
|
||||
viewBody()
|
||||
.onAppear {
|
||||
// for direct chat ConnectionStats are populated on opening chat, see ChatView onAppear
|
||||
if case let .group(groupInfo, _) = chat.chatInfo,
|
||||
if case let .group(groupInfo, _, _) = chat.chatInfo,
|
||||
case let .groupRcv(groupMember) = chatItem.chatDir {
|
||||
do {
|
||||
let (member, stats) = try apiGroupMemberInfoSync(groupInfo.apiId, groupMember.groupMemberId)
|
||||
@@ -85,7 +85,7 @@ struct CIRcvDecryptionError: View {
|
||||
} else {
|
||||
basicDecryptionErrorItem()
|
||||
}
|
||||
} else if case let .group(groupInfo, _) = chat.chatInfo,
|
||||
} else if case let .group(groupInfo, _, _) = chat.chatInfo,
|
||||
case let .groupRcv(groupMember) = chatItem.chatDir,
|
||||
let mem = m.getGroupMember(groupMember.groupMemberId),
|
||||
let memberStats = mem.wrapped.activeConn?.connectionStats {
|
||||
|
||||
@@ -313,7 +313,7 @@ struct FramedItemView: View {
|
||||
|
||||
private func membership() -> GroupMember? {
|
||||
switch chat.chatInfo {
|
||||
case let .group(groupInfo: groupInfo, _): return groupInfo.membership
|
||||
case let .group(groupInfo: groupInfo, _, _): return groupInfo.membership
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ struct ChatView: View {
|
||||
private var viewBody: some View {
|
||||
let cInfo = chat.chatInfo
|
||||
let memberSupportChat: (groupInfo: GroupInfo, member: GroupMember?)? =
|
||||
if case let .group(groupInfo, .memberSupport(member)) = cInfo {
|
||||
if case let .group(groupInfo, .memberSupport(member), _) = cInfo {
|
||||
(groupInfo, member)
|
||||
} else {
|
||||
nil
|
||||
@@ -156,7 +156,7 @@ struct ChatView: View {
|
||||
showArchiveSelectedReports = true
|
||||
},
|
||||
moderateItems: {
|
||||
if case let .group(groupInfo, _) = chat.chatInfo {
|
||||
if case let .group(groupInfo, _, _) = chat.chatInfo {
|
||||
showModerateSelectedMessagesAlert(groupInfo)
|
||||
}
|
||||
},
|
||||
@@ -167,7 +167,7 @@ struct ChatView: View {
|
||||
if im.showLoadingProgress == chat.id {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
if case let .group(groupInfo, _) = chat.chatInfo,
|
||||
if case let .group(groupInfo, _, _) = chat.chatInfo,
|
||||
case let .groupChatScopeContext(groupScopeInfo) = im.secondaryIMFilter,
|
||||
case let .memberSupport(groupMember_) = groupScopeInfo,
|
||||
let groupMember = groupMember_ {
|
||||
@@ -225,7 +225,7 @@ struct ChatView: View {
|
||||
archiveReports(chat, selected.sorted(), false, deletedSelectedMessages)
|
||||
}
|
||||
}
|
||||
if case let ChatInfo.group(groupInfo, _) = chat.chatInfo, groupInfo.membership.memberActive {
|
||||
if case let ChatInfo.group(groupInfo, _, _) = chat.chatInfo, groupInfo.membership.memberActive {
|
||||
Button("For all moderators", role: .destructive) {
|
||||
if let selected = selectedChatItems {
|
||||
archiveReports(chat, selected.sorted(), true, deletedSelectedMessages)
|
||||
@@ -236,7 +236,7 @@ struct ChatView: View {
|
||||
.appSheet(item: $selectedMember, onDismiss: {
|
||||
chatModel.secondaryIM = nil
|
||||
}) { member in
|
||||
if case let .group(groupInfo, _) = chat.chatInfo {
|
||||
if case let .group(groupInfo, _, _) = chat.chatInfo {
|
||||
GroupMemberInfoView(
|
||||
groupInfo: groupInfo,
|
||||
chat: chat,
|
||||
@@ -248,12 +248,12 @@ struct ChatView: View {
|
||||
}
|
||||
// it should be presented on top level in order to prevent a bug in SwiftUI on iOS 16 related to .focused() modifier in AddGroupMembersView's search field
|
||||
.appSheet(isPresented: $showAddMembersSheet) {
|
||||
if case let .group(groupInfo, _) = cInfo {
|
||||
if case let .group(groupInfo, _, _) = cInfo {
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
|
||||
}
|
||||
}
|
||||
.appSheet(isPresented: $showGroupLinkSheet) {
|
||||
if case let .group(groupInfo, _) = cInfo {
|
||||
if case let .group(groupInfo, _, _) = cInfo {
|
||||
GroupLinkView(
|
||||
groupId: groupInfo.groupId,
|
||||
groupLink: $groupLink,
|
||||
@@ -292,7 +292,7 @@ struct ChatView: View {
|
||||
) {
|
||||
if let groupInfo = cInfo.groupInfo {
|
||||
SecondaryChatView(
|
||||
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: userSupportScopeInfo), chatItems: [], chatStats: ChatStats()),
|
||||
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: userSupportScopeInfo, channelMsgInfo: nil), chatItems: [], chatStats: ChatStats()),
|
||||
scrollToItemId: $scrollToItemId
|
||||
)
|
||||
}
|
||||
@@ -314,7 +314,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
// if this is the main chat of the group with the pending member (knocking)
|
||||
if case let .group(groupInfo, nil) = chat.chatInfo,
|
||||
if case let .group(groupInfo, nil, _) = chat.chatInfo,
|
||||
groupInfo.membership.memberPending {
|
||||
ItemsModel.loadSecondaryChat(chat.id, chatFilter: .groupChatScopeContext(groupScopeInfo: userSupportScopeInfo)) {
|
||||
showUserSupportChatSheet = true
|
||||
@@ -492,7 +492,7 @@ struct ChatView: View {
|
||||
onSearch: { focusSearch() }
|
||||
)
|
||||
}
|
||||
} else if case let .group(groupInfo, _) = cInfo {
|
||||
} else if case let .group(groupInfo, _, _) = cInfo {
|
||||
Button {
|
||||
Task { await chatModel.loadGroupMembers(groupInfo) { showChatInfoSheet = true } }
|
||||
} label: {
|
||||
@@ -508,7 +508,7 @@ struct ChatView: View {
|
||||
groupInfo: Binding(
|
||||
get: { groupInfo },
|
||||
set: { gInfo in
|
||||
chat.chatInfo = .group(groupInfo: gInfo, groupChatScope: nil)
|
||||
chat.chatInfo = .group(groupInfo: gInfo, groupChatScope: nil, channelMsgInfo: nil)
|
||||
chat.created = Date.now
|
||||
}
|
||||
),
|
||||
@@ -588,7 +588,7 @@ struct ChatView: View {
|
||||
Image(systemName: "ellipsis")
|
||||
}
|
||||
}
|
||||
case let .group(groupInfo, _):
|
||||
case let .group(groupInfo, _, _):
|
||||
HStack {
|
||||
contentFilterMenu(withLabel: false)
|
||||
Menu {
|
||||
@@ -736,7 +736,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
if case let .group(groupInfo, _) = cInfo, groupInfo.useRelays {
|
||||
if case let .group(groupInfo, _, _) = cInfo, groupInfo.useRelays {
|
||||
Task { await chatModel.loadGroupMembers(groupInfo) }
|
||||
if groupInfo.membership.memberRole == .owner {
|
||||
Task {
|
||||
@@ -1047,7 +1047,7 @@ struct ChatView: View {
|
||||
return ("SimpleX address", connLink)
|
||||
}
|
||||
}
|
||||
case let .group(groupInfo, _):
|
||||
case let .group(groupInfo, _, _):
|
||||
if !groupInfo.nextConnectPrepared {
|
||||
if let pg = groupInfo.preparedGroup {
|
||||
let connLink = pg.connLinkToConnect.simplexChatUri()
|
||||
@@ -1080,7 +1080,7 @@ struct ChatView: View {
|
||||
} else {
|
||||
"Your contact"
|
||||
}
|
||||
case let .group(groupInfo, _):
|
||||
case let .group(groupInfo, _, _):
|
||||
switch groupInfo.businessChat?.chatType {
|
||||
case .none:
|
||||
if groupInfo.nextConnectPrepared {
|
||||
@@ -1116,7 +1116,7 @@ struct ChatView: View {
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
case let .group(groupInfo, _):
|
||||
case let .group(groupInfo, _, _):
|
||||
if groupInfo.useRelays {
|
||||
nil
|
||||
} else {
|
||||
@@ -1419,7 +1419,7 @@ struct ChatView: View {
|
||||
|
||||
private func addMembersButton() -> some View {
|
||||
Button {
|
||||
if case let .group(gInfo, _) = chat.chatInfo {
|
||||
if case let .group(gInfo, _, _) = chat.chatInfo {
|
||||
Task { await chatModel.loadGroupMembers(gInfo) { showAddMembersSheet = true } }
|
||||
}
|
||||
} label: {
|
||||
@@ -1429,7 +1429,7 @@ struct ChatView: View {
|
||||
|
||||
private func groupLinkButton() -> some View {
|
||||
Button {
|
||||
if case let .group(gInfo, _) = chat.chatInfo {
|
||||
if case let .group(gInfo, _, _) = chat.chatInfo {
|
||||
Task {
|
||||
do {
|
||||
if let gLink = try apiGetGroupLink(gInfo.groupId) {
|
||||
@@ -1443,7 +1443,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
if case let .group(gInfo, _) = chat.chatInfo, gInfo.useRelays {
|
||||
if case let .group(gInfo, _, _) = chat.chatInfo, gInfo.useRelays {
|
||||
Label("Channel link", systemImage: "link")
|
||||
} else {
|
||||
Label("Group link", systemImage: "link.badge.plus")
|
||||
@@ -1916,7 +1916,7 @@ struct ChatView: View {
|
||||
) -> some View {
|
||||
let bottomPadding: Double = itemSeparation.largeGap ? 10 : 2
|
||||
if case .channelRcv = ci.chatDir,
|
||||
case let .group(groupInfo, _) = chat.chatInfo {
|
||||
case let .group(groupInfo, _, _) = chat.chatInfo {
|
||||
if showAvatar {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if ci.content.showMemberName {
|
||||
@@ -1983,7 +1983,7 @@ struct ChatView: View {
|
||||
.padding(.bottom, bottomPadding)
|
||||
}
|
||||
} else if case let .groupRcv(member) = ci.chatDir,
|
||||
case let .group(groupInfo, _) = chat.chatInfo {
|
||||
case let .group(groupInfo, _, _) = chat.chatInfo {
|
||||
if showAvatar {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if ci.content.showMemberName {
|
||||
@@ -2196,7 +2196,7 @@ struct ChatView: View {
|
||||
self.archivingReports = []
|
||||
}
|
||||
}
|
||||
if case let ChatInfo.group(groupInfo, _) = chat.chatInfo, groupInfo.membership.memberActive {
|
||||
if case let ChatInfo.group(groupInfo, _, _) = chat.chatInfo, groupInfo.membership.memberActive {
|
||||
Button("For all moderators", role: .destructive) {
|
||||
if let reports = self.archivingReports {
|
||||
archiveReports(chat, reports.sorted(), true)
|
||||
@@ -2246,7 +2246,7 @@ struct ChatView: View {
|
||||
})
|
||||
}
|
||||
switch chat.chatInfo {
|
||||
case let .group(groupInfo, _):
|
||||
case let .group(groupInfo, _, _):
|
||||
v.contextMenu {
|
||||
ReactionContextMenu(
|
||||
groupInfo: groupInfo,
|
||||
@@ -2269,7 +2269,7 @@ struct ChatView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private func menu(_ ci: ChatItem, _ range: ClosedRange<Int>?, live: Bool) -> some View {
|
||||
if case let .group(gInfo, _) = chat.chatInfo, ci.isReport, ci.meta.itemDeleted == nil {
|
||||
if case let .group(gInfo, _, _) = chat.chatInfo, ci.isReport, ci.meta.itemDeleted == nil {
|
||||
if ci.chatDir != .groupSnd, gInfo.membership.memberRole >= .moderator {
|
||||
archiveReportButton(ci)
|
||||
}
|
||||
@@ -2328,7 +2328,7 @@ struct ChatView: View {
|
||||
if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) {
|
||||
moderateButton(ci, groupInfo)
|
||||
} else if ci.meta.itemDeleted == nil && chat.groupFeatureEnabled(.reports),
|
||||
case let .group(gInfo, _) = chat.chatInfo,
|
||||
case let .group(gInfo, _, _) = chat.chatInfo,
|
||||
gInfo.membership.memberRole == .member
|
||||
&& !live
|
||||
&& composeState.voiceMessageRecordingState == .noRecording {
|
||||
@@ -2557,7 +2557,7 @@ struct ChatView: View {
|
||||
await MainActor.run {
|
||||
chatItemInfo = ciInfo
|
||||
}
|
||||
if case let .group(gInfo, _) = chat.chatInfo {
|
||||
if case let .group(gInfo, _, _) = chat.chatInfo {
|
||||
await m.loadGroupMembers(gInfo)
|
||||
}
|
||||
} catch let error {
|
||||
@@ -3054,7 +3054,7 @@ private func buildTheme() -> AppTheme {
|
||||
if let cId = ChatModel.shared.chatId, let chat = ChatModel.shared.getChat(cId) {
|
||||
let perChatTheme = if case let .direct(contact) = chat.chatInfo {
|
||||
contact.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight)
|
||||
} else if case let .group(groupInfo, _) = chat.chatInfo {
|
||||
} else if case let .group(groupInfo, _, _) = chat.chatInfo {
|
||||
groupInfo.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight)
|
||||
} else {
|
||||
nil as ThemeModeOverride?
|
||||
@@ -3208,7 +3208,7 @@ func updateChatSettings(_ chat: Chat, chatSettings: ChatSettings) {
|
||||
case var .direct(contact):
|
||||
contact.chatSettings = chatSettings
|
||||
ChatModel.shared.updateContact(contact)
|
||||
case var .group(groupInfo, _):
|
||||
case var .group(groupInfo, _, _):
|
||||
groupInfo.chatSettings = chatSettings
|
||||
ChatModel.shared.updateGroup(groupInfo)
|
||||
default: ()
|
||||
|
||||
@@ -699,7 +699,7 @@ struct GroupChatInfoView: View {
|
||||
let scopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: nil)
|
||||
NavigationLink(isActive: $navLinkActive) {
|
||||
SecondaryChatView(
|
||||
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo), chatItems: [], chatStats: ChatStats()),
|
||||
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo, channelMsgInfo: nil), chatItems: [], chatStats: ChatStats()),
|
||||
scrollToItemId: $scrollToItemId
|
||||
)
|
||||
} label: {
|
||||
@@ -750,7 +750,7 @@ struct GroupChatInfoView: View {
|
||||
var body: some View {
|
||||
NavigationLink(isActive: $navLinkActive) {
|
||||
SecondaryChatView(
|
||||
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: .reports), chatItems: [], chatStats: ChatStats()),
|
||||
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: .reports, channelMsgInfo: nil), chatItems: [], chatStats: ChatStats()),
|
||||
scrollToItemId: $scrollToItemId
|
||||
)
|
||||
} label: {
|
||||
|
||||
@@ -352,7 +352,7 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
}
|
||||
.onChange(of: chat.chatInfo) { c in
|
||||
if case let .group(gI, _) = chat.chatInfo {
|
||||
if case let .group(gI, _, _) = chat.chatInfo {
|
||||
groupInfo = gI
|
||||
}
|
||||
}
|
||||
@@ -566,7 +566,7 @@ struct GroupMemberInfoView: View {
|
||||
let scopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: member.wrapped)
|
||||
NavigationLink(isActive: $navLinkActive) {
|
||||
SecondaryChatView(
|
||||
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo), chatItems: [], chatStats: ChatStats()),
|
||||
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo, channelMsgInfo: nil), chatItems: [], chatStats: ChatStats()),
|
||||
scrollToItemId: $scrollToItemId
|
||||
)
|
||||
} label: {
|
||||
|
||||
@@ -83,7 +83,7 @@ struct MemberSupportView: View {
|
||||
|
||||
NavigationLink(isActive: $memberSupportChatNavLinkActive) {
|
||||
SecondaryChatView(
|
||||
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo), chatItems: [], chatStats: ChatStats()),
|
||||
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo, channelMsgInfo: nil), chatItems: [], chatStats: ChatStats()),
|
||||
scrollToItemId: $scrollToItemId
|
||||
)
|
||||
} label: {
|
||||
|
||||
@@ -35,7 +35,7 @@ struct SecondaryChatView: View {
|
||||
#Preview {
|
||||
SecondaryChatView(
|
||||
chat: Chat(
|
||||
chatInfo: .group(groupInfo: GroupInfo.sampleData, groupChatScope: .memberSupport(groupMember_: GroupMember.sampleData)),
|
||||
chatInfo: .group(groupInfo: GroupInfo.sampleData, groupChatScope: .memberSupport(groupMember_: GroupMember.sampleData), channelMsgInfo: nil),
|
||||
chatItems: [],
|
||||
chatStats: ChatStats()
|
||||
),
|
||||
|
||||
@@ -116,7 +116,7 @@ struct SelectedItemsBottomToolbar: View {
|
||||
deleteCountProhibited = count == 0 || count > 200
|
||||
forwardCountProhibited = count == 0 || count > 20
|
||||
canModerate = possibleToModerate(chatInfo)
|
||||
let groupInfo: GroupInfo? = if case let ChatInfo.group(groupInfo: info, _) = chatInfo {
|
||||
let groupInfo: GroupInfo? = if case let ChatInfo.group(groupInfo: info, _, _) = chatInfo {
|
||||
info
|
||||
} else {
|
||||
nil
|
||||
@@ -145,7 +145,7 @@ struct SelectedItemsBottomToolbar: View {
|
||||
|
||||
private func possibleToModerate(_ chatInfo: ChatInfo) -> Bool {
|
||||
return switch chatInfo {
|
||||
case let .group(groupInfo, _):
|
||||
case let .group(groupInfo, _, _):
|
||||
groupInfo.membership.memberRole >= .moderator
|
||||
default: false
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ struct ChatListNavLink: View {
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact):
|
||||
contactNavLink(contact)
|
||||
case let .group(groupInfo, _):
|
||||
case let .group(groupInfo, _, _):
|
||||
groupNavLink(groupInfo)
|
||||
case let .local(noteFolder):
|
||||
noteFolderNavLink(noteFolder)
|
||||
|
||||
@@ -952,17 +952,17 @@ func presetTagMatchesChat(_ tag: PresetTag, _ chatInfo: ChatInfo, _ chatStats: C
|
||||
case let .direct(contact): !contact.isContactCard && !contact.chatDeleted
|
||||
case .contactRequest: true
|
||||
case .contactConnection: true
|
||||
case let .group(groupInfo, _): groupInfo.businessChat?.chatType == .customer
|
||||
case let .group(groupInfo, _, _): groupInfo.businessChat?.chatType == .customer
|
||||
default: false
|
||||
}
|
||||
case .groups:
|
||||
switch chatInfo {
|
||||
case let .group(groupInfo, _): groupInfo.businessChat == nil && !groupInfo.isChannel
|
||||
case let .group(groupInfo, _, _): groupInfo.businessChat == nil && !groupInfo.isChannel
|
||||
default: false
|
||||
}
|
||||
case .channels:
|
||||
switch chatInfo {
|
||||
case let .group(groupInfo, _): groupInfo.isChannel
|
||||
case let .group(groupInfo, _, _): groupInfo.isChannel
|
||||
default: false
|
||||
}
|
||||
case .business:
|
||||
|
||||
@@ -142,7 +142,7 @@ struct ChatPreviewView: View {
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
case let .group(groupInfo, _):
|
||||
case let .group(groupInfo, _, _):
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
case .memRejected: inactiveIcon()
|
||||
case .memLeft: inactiveIcon()
|
||||
@@ -174,7 +174,7 @@ struct ChatPreviewView: View {
|
||||
? theme.colors.secondary
|
||||
: nil
|
||||
previewTitle(contact.verified == true ? verifiedIcon + t : t).foregroundColor(color)
|
||||
case let .group(groupInfo, _):
|
||||
case let .group(groupInfo, _, _):
|
||||
let color = if deleting {
|
||||
theme.colors.secondary
|
||||
} else {
|
||||
@@ -377,7 +377,7 @@ struct ChatPreviewView: View {
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
case let .group(groupInfo, _):
|
||||
case let .group(groupInfo, _, _):
|
||||
if groupInfo.nextConnectPrepared {
|
||||
if groupInfo.businessChat?.chatType == .business {
|
||||
Text("Open to connect")
|
||||
|
||||
@@ -204,7 +204,7 @@ struct AddGroupView: View {
|
||||
Task {
|
||||
await m.loadGroupMembers(gInfo)
|
||||
}
|
||||
let c = Chat(chatInfo: .group(groupInfo: gInfo, groupChatScope: nil), chatItems: [])
|
||||
let c = Chat(chatInfo: .group(groupInfo: gInfo, groupChatScope: nil, channelMsgInfo: nil), chatItems: [])
|
||||
m.addChat(c)
|
||||
withAnimation {
|
||||
groupInfo = gInfo
|
||||
|
||||
@@ -1202,7 +1202,7 @@ private func showPrepareGroupAlert(
|
||||
let chat = try await apiPrepareGroup(connLink: connectionLink, directLink: groupShortLinkInfo?.direct ?? true, groupShortLinkData: groupShortLinkData)
|
||||
await MainActor.run {
|
||||
if let relays = groupShortLinkInfo?.groupRelays, !relays.isEmpty,
|
||||
case let .group(gInfo, _) = chat.chatInfo {
|
||||
case let .group(gInfo, _, _) = chat.chatInfo {
|
||||
ChatModel.shared.channelRelayHostnames[gInfo.groupId] = relays
|
||||
}
|
||||
ChatModel.shared.addChat(Chat(chat))
|
||||
|
||||
@@ -1434,7 +1434,7 @@ public enum GroupFeatureEnabled: String, Codable, Identifiable, Hashable {
|
||||
// Spec: spec/state.md#ChatInfo
|
||||
public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
case direct(contact: Contact)
|
||||
case group(groupInfo: GroupInfo, groupChatScope: GroupChatScopeInfo?)
|
||||
case group(groupInfo: GroupInfo, groupChatScope: GroupChatScopeInfo?, channelMsgInfo: ChannelMsgInfo?)
|
||||
case local(noteFolder: NoteFolder)
|
||||
case contactRequest(contactRequest: UserContactRequest)
|
||||
case contactConnection(contactConnection: PendingContactConnection)
|
||||
@@ -1448,7 +1448,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.localDisplayName
|
||||
case let .group(groupInfo, _): return groupInfo.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
|
||||
@@ -1461,7 +1461,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.displayName
|
||||
case let .group(groupInfo, _): return groupInfo.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
|
||||
@@ -1474,7 +1474,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.fullName
|
||||
case let .group(groupInfo, _): return groupInfo.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
|
||||
@@ -1486,7 +1486,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
public var shortDescr: String? {
|
||||
switch self {
|
||||
case let .direct(contact): contact.profile.shortDescr
|
||||
case let .group(groupInfo, _): groupInfo.groupProfile.shortDescr
|
||||
case let .group(groupInfo, _, _): groupInfo.groupProfile.shortDescr
|
||||
case .local: nil
|
||||
case let .contactRequest(contactRequest): contactRequest.profile.shortDescr
|
||||
case let .contactConnection(contactConnection): nil
|
||||
@@ -1498,7 +1498,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.image
|
||||
case let .group(groupInfo, _): return groupInfo.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
|
||||
@@ -1511,7 +1511,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.localAlias
|
||||
case let .group(groupInfo, _): return groupInfo.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
|
||||
@@ -1524,7 +1524,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.id
|
||||
case let .group(groupInfo, _): return groupInfo.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
|
||||
@@ -1550,7 +1550,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.apiId
|
||||
case let .group(groupInfo, _): return groupInfo.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
|
||||
@@ -1563,7 +1563,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.ready
|
||||
case let .group(groupInfo, _): return groupInfo.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
|
||||
@@ -1575,7 +1575,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
public var sndReady: Bool {
|
||||
switch self {
|
||||
case let .direct(contact): contact.sndReady
|
||||
case let .group(groupInfo, groupScope):
|
||||
case let .group(groupInfo, groupScope, _):
|
||||
groupInfo.membership.memberActive
|
||||
&& (groupScope != nil || (!groupInfo.membership.memberPending && groupInfo.membership.memberRole != .observer))
|
||||
case .local: true
|
||||
@@ -1598,7 +1598,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.sendMsgToConnect
|
||||
case let .group(groupInfo, _): return groupInfo.nextConnectPrepared
|
||||
case let .group(groupInfo, _, _): return groupInfo.nextConnectPrepared
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
@@ -1608,7 +1608,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.nextConnectPrepared
|
||||
case let .group(groupInfo, _): return groupInfo.nextConnectPrepared
|
||||
case let .group(groupInfo, _, _): return groupInfo.nextConnectPrepared
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
@@ -1618,7 +1618,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.profileChangeProhibited
|
||||
case let .group(groupInfo, _): return groupInfo.profileChangeProhibited
|
||||
case let .group(groupInfo, _, _): return groupInfo.profileChangeProhibited
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
@@ -1634,7 +1634,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
if contact.activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false { return ("not synchronized", nil) }
|
||||
if contact.activeConn?.connDisabled ?? true { return ("contact disabled", nil) }
|
||||
return nil
|
||||
case let .group(groupInfo, groupChatScope):
|
||||
case let .group(groupInfo, groupChatScope, _):
|
||||
if groupInfo.membership.memberActive {
|
||||
switch(groupChatScope) {
|
||||
case .none:
|
||||
@@ -1682,7 +1682,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.contactConnIncognito
|
||||
case let .group(groupInfo, _): return groupInfo.membership.memberIncognito
|
||||
case let .group(groupInfo, _, _): return groupInfo.membership.memberIncognito
|
||||
case .local: return false
|
||||
case .contactRequest: return false
|
||||
case let .contactConnection(contactConnection): return contactConnection.incognito
|
||||
@@ -1707,7 +1707,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
|
||||
public var groupInfo: GroupInfo? {
|
||||
switch self {
|
||||
case let .group(groupInfo, _): return groupInfo
|
||||
case let .group(groupInfo, _, _): return groupInfo
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
@@ -1729,7 +1729,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
case .files: return cups.files.enabled.forUser
|
||||
case .calls: return cups.calls.enabled.forUser
|
||||
}
|
||||
case let .group(groupInfo, _):
|
||||
case let .group(groupInfo, _, _):
|
||||
let prefs = groupInfo.fullGroupPreferences
|
||||
switch feature {
|
||||
case .timedMessages: return prefs.timedMessages.on
|
||||
@@ -1757,7 +1757,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
case let .direct(contact):
|
||||
let pref = contact.mergedPreferences.timedMessages
|
||||
return pref.enabled.forUser ? pref.userPreference.preference.ttl : nil
|
||||
case let .group(groupInfo, _):
|
||||
case let .group(groupInfo, _, _):
|
||||
let pref = groupInfo.fullGroupPreferences.timedMessages
|
||||
return pref.on ? pref.ttl : nil
|
||||
default:
|
||||
@@ -1782,7 +1782,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
} else {
|
||||
return .other
|
||||
}
|
||||
case let .group(groupInfo, _):
|
||||
case let .group(groupInfo, _, _):
|
||||
if !groupInfo.fullGroupPreferences.voice.on(for: groupInfo.membership) {
|
||||
return .groupOwnerCan
|
||||
} else {
|
||||
@@ -1816,7 +1816,16 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
|
||||
public func groupChatScope() -> GroupChatScope? {
|
||||
switch self {
|
||||
case let .group(_, groupChatScope): groupChatScope?.toChatScope()
|
||||
case let .group(_, groupChatScope, _): groupChatScope?.toChatScope()
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
// Local-only carrier for the comments-thread view, embedded by
|
||||
// ItemsModel.loadSecondaryChat. Nil for main-chat / member-support / direct / etc.
|
||||
public func channelMsgInfo() -> ChannelMsgInfo? {
|
||||
switch self {
|
||||
case let .group(_, _, channelMsgInfo): channelMsgInfo
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
@@ -1848,7 +1857,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
public var chatSettings: ChatSettings? {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.chatSettings
|
||||
case let .group(groupInfo, _): return groupInfo.chatSettings
|
||||
case let .group(groupInfo, _, _): return groupInfo.chatSettings
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
@@ -1864,7 +1873,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
public var useCommands: Bool {
|
||||
switch self {
|
||||
case let .direct(c): c.isBot
|
||||
case let .group(g, _): (g.groupProfile.groupPreferences?.commands?.count ?? 0) > 0
|
||||
case let .group(g, _, _): (g.groupProfile.groupPreferences?.commands?.count ?? 0) > 0
|
||||
default: false
|
||||
}
|
||||
}
|
||||
@@ -1872,7 +1881,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
public var menuCommands: [ChatBotCommand] {
|
||||
switch self {
|
||||
case let .direct(c): c.isBot ? c.profile.preferences?.commands ?? [] : []
|
||||
case let .group(g, _): g.groupProfile.groupPreferences?.commands ?? []
|
||||
case let .group(g, _, _): g.groupProfile.groupPreferences?.commands ?? []
|
||||
default: []
|
||||
}
|
||||
}
|
||||
@@ -1880,7 +1889,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
public var chatTags: [Int64]? {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.chatTags
|
||||
case let .group(groupInfo, _): return groupInfo.chatTags
|
||||
case let .group(groupInfo, _, _): return groupInfo.chatTags
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
@@ -1888,7 +1897,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
public var chatTs: Date {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.chatTs ?? contact.updatedAt
|
||||
case let .group(groupInfo, _): return groupInfo.chatTs ?? groupInfo.updatedAt
|
||||
case let .group(groupInfo, _, _): return groupInfo.chatTs ?? groupInfo.updatedAt
|
||||
case let .local(noteFolder): return noteFolder.chatTs
|
||||
case let .contactRequest(contactRequest): return contactRequest.updatedAt
|
||||
case let .contactConnection(contactConnection): return contactConnection.updatedAt
|
||||
@@ -1904,7 +1913,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
} else {
|
||||
ChatTTL.userDefault(globalTTL)
|
||||
}
|
||||
case let .group(groupInfo, _):
|
||||
case let .group(groupInfo, _, _):
|
||||
return if let ciTTL = groupInfo.chatItemTTL {
|
||||
ChatTTL.chat(ChatItemTTL(ciTTL))
|
||||
} else {
|
||||
@@ -1924,7 +1933,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
|
||||
public static var sampleData: ChatInfo.SampleData = SampleData(
|
||||
direct: ChatInfo.direct(contact: Contact.sampleData),
|
||||
group: ChatInfo.group(groupInfo: GroupInfo.sampleData, groupChatScope: nil),
|
||||
group: ChatInfo.group(groupInfo: GroupInfo.sampleData, groupChatScope: nil, channelMsgInfo: nil),
|
||||
local: ChatInfo.local(noteFolder: NoteFolder.sampleData),
|
||||
contactRequest: ChatInfo.contactRequest(contactRequest: UserContactRequest.sampleData),
|
||||
contactConnection: ChatInfo.contactConnection(contactConnection: PendingContactConnection.getSampleData())
|
||||
@@ -2013,6 +2022,19 @@ public enum GroupChatScopeInfo: Decodable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
// Local-only carrier for the comments-thread view; not present on the wire.
|
||||
// Embedded in `ChatInfo.group`'s third associated value by `ItemsModel.loadSecondaryChat`
|
||||
// after fetching the chat by parent= pagination. See spec/state.md#ChannelMsgInfo.
|
||||
public struct ChannelMsgInfo: Decodable, Hashable {
|
||||
public var channelMsgItem: ChatItem
|
||||
public var channelMsgSharedId: String
|
||||
|
||||
public init(channelMsgItem: ChatItem, channelMsgSharedId: String) {
|
||||
self.channelMsgItem = channelMsgItem
|
||||
self.channelMsgSharedId = channelMsgSharedId
|
||||
}
|
||||
}
|
||||
|
||||
public struct Contact: Identifiable, Decodable, NamedChat, Hashable {
|
||||
public var contactId: Int64
|
||||
var localDisplayName: ContactName
|
||||
@@ -3239,7 +3261,7 @@ public struct CIReaction: Decodable, Hashable {
|
||||
}
|
||||
|
||||
public struct ChatItem: Identifiable, Decodable, Hashable {
|
||||
public init(chatDir: CIDirection, meta: CIMeta, content: CIContent, formattedText: [FormattedText]? = nil, mentions: [String: CIMention]? = nil, quotedItem: CIQuote? = nil, reactions: [CIReactionCount] = [], file: CIFile? = nil) {
|
||||
public init(chatDir: CIDirection, meta: CIMeta, content: CIContent, formattedText: [FormattedText]? = nil, mentions: [String: CIMention]? = nil, quotedItem: CIQuote? = nil, reactions: [CIReactionCount] = [], file: CIFile? = nil, parentChatItemId: Int64? = nil, commentsTotal: Int = 0, commentsDisabled: Bool = false) {
|
||||
self.chatDir = chatDir
|
||||
self.meta = meta
|
||||
self.content = content
|
||||
@@ -3248,6 +3270,9 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
|
||||
self.quotedItem = quotedItem
|
||||
self.reactions = reactions
|
||||
self.file = file
|
||||
self.parentChatItemId = parentChatItemId
|
||||
self.commentsTotal = commentsTotal
|
||||
self.commentsDisabled = commentsDisabled
|
||||
}
|
||||
|
||||
public var chatDir: CIDirection
|
||||
@@ -3258,6 +3283,12 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
|
||||
public var quotedItem: CIQuote?
|
||||
public var reactions: [CIReactionCount]
|
||||
public var file: CIFile?
|
||||
// Slice-1 channel-comments fields. On the wire these live inside the `meta`
|
||||
// object (Haskell `CIMeta`); for iOS caller ergonomics they are hoisted to
|
||||
// ChatItem level. See spec/state.md#ChatItem and plan §5.1 item 4.
|
||||
public var parentChatItemId: Int64? = nil
|
||||
public var commentsTotal: Int = 0
|
||||
public var commentsDisabled: Bool = false
|
||||
|
||||
public var viewTimestamp = Date.now
|
||||
public var isLiveDummy: Bool = false
|
||||
@@ -3266,8 +3297,37 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
|
||||
case chatDir, meta, content, formattedText, mentions, quotedItem, reactions, file
|
||||
}
|
||||
|
||||
private enum MetaForwardKeys: String, CodingKey {
|
||||
case parentChatItemId, commentsTotal, commentsDisabled
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.chatDir = try container.decode(CIDirection.self, forKey: .chatDir)
|
||||
self.meta = try container.decode(CIMeta.self, forKey: .meta)
|
||||
self.content = try container.decode(CIContent.self, forKey: .content)
|
||||
self.formattedText = try container.decodeIfPresent([FormattedText].self, forKey: .formattedText)
|
||||
self.mentions = try container.decodeIfPresent([String: CIMention].self, forKey: .mentions)
|
||||
self.quotedItem = try container.decodeIfPresent(CIQuote.self, forKey: .quotedItem)
|
||||
self.reactions = try container.decodeIfPresent([CIReactionCount].self, forKey: .reactions) ?? []
|
||||
self.file = try container.decodeIfPresent(CIFile.self, forKey: .file)
|
||||
// Hoist the three channel-comments fields from the nested `meta` object.
|
||||
// Defaults mirror Haskell `omittedField`: nil / 0 / false.
|
||||
let metaContainer = try container.nestedContainer(keyedBy: MetaForwardKeys.self, forKey: .meta)
|
||||
self.parentChatItemId = try metaContainer.decodeIfPresent(Int64.self, forKey: .parentChatItemId)
|
||||
self.commentsTotal = try metaContainer.decodeIfPresent(Int.self, forKey: .commentsTotal) ?? 0
|
||||
self.commentsDisabled = try metaContainer.decodeIfPresent(Bool.self, forKey: .commentsDisabled) ?? false
|
||||
}
|
||||
|
||||
public var id: Int64 { meta.itemId }
|
||||
|
||||
// Subscriber-view helper: true iff this item came in on the channel-cast direction.
|
||||
// Owner-side composers determine the equivalent locally (groupInfo.useRelays && chatDir == .groupSnd).
|
||||
public var isChannelPost: Bool {
|
||||
if case .channelRcv = chatDir { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
public var viewId: String { "\(meta.itemId) \(viewTimestamp.timeIntervalSince1970)" }
|
||||
|
||||
public var timestampText: Text { meta.timestampText }
|
||||
@@ -3444,12 +3504,12 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
|
||||
|
||||
public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember?)? {
|
||||
switch (chatInfo, chatDir) {
|
||||
case let (.group(groupInfo, _), .groupRcv(groupMember)):
|
||||
case let (.group(groupInfo, _, _), .groupRcv(groupMember)):
|
||||
let m = groupInfo.membership
|
||||
return m.memberRole >= .moderator && m.memberRole >= groupMember.memberRole && meta.itemDeleted == nil
|
||||
? (groupInfo, groupMember)
|
||||
: nil
|
||||
case let (.group(groupInfo, _), .groupSnd):
|
||||
case let (.group(groupInfo, _, _), .groupSnd):
|
||||
let m = groupInfo.membership
|
||||
return m.memberRole >= .moderator ? (groupInfo, nil) : nil
|
||||
case (.group, .channelRcv):
|
||||
@@ -3718,6 +3778,9 @@ public struct CIMeta: Decodable, Hashable {
|
||||
public var deletable: Bool
|
||||
public var editable: Bool
|
||||
public var showGroupAsSender: Bool
|
||||
// Wire field present once an item has been propagated to the relay/peer.
|
||||
// Optional because items in "sending" state may not yet have one.
|
||||
public var itemSharedMsgId: String? = nil
|
||||
|
||||
public var timestampText: Text { Text(formatTimestampMeta(itemTs)) }
|
||||
public var recent: Bool { updatedAt + 10 > .now }
|
||||
|
||||
@@ -14,7 +14,7 @@ public protocol ChatLike {
|
||||
|
||||
extension ChatLike {
|
||||
public func groupFeatureEnabled(_ feature: GroupFeature) -> Bool {
|
||||
if case let .group(groupInfo, _) = self.chatInfo {
|
||||
if case let .group(groupInfo, _, _) = self.chatInfo {
|
||||
let p = groupInfo.fullGroupPreferences
|
||||
return switch feature {
|
||||
case .timedMessages: p.timedMessages.on
|
||||
@@ -93,7 +93,7 @@ private func canForwardToChat(_ cInfo: ChatInfo) -> Bool {
|
||||
public func chatIconName(_ cInfo: ChatInfo) -> String {
|
||||
switch cInfo {
|
||||
case let .direct(contact): contact.chatIconName
|
||||
case let .group(groupInfo, _): groupInfo.chatIconName
|
||||
case let .group(groupInfo, _, _): groupInfo.chatIconName
|
||||
case .local: "folder.circle.fill"
|
||||
case .contactRequest: "person.crop.circle.fill"
|
||||
default: "circle.fill"
|
||||
|
||||
@@ -66,7 +66,7 @@ public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact,
|
||||
public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem, _ badgeCount: Int) -> UNMutableNotificationContent {
|
||||
let previewMode = ntfPreviewModeGroupDefault.get()
|
||||
var title: String
|
||||
if case let .group(groupInfo, _) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir {
|
||||
if case let .group(groupInfo, _, _) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir {
|
||||
title = groupMsgNtfTitle(groupInfo, groupMember, hideContent: previewMode == .hidden)
|
||||
} else {
|
||||
title = previewMode == .hidden ? contactHidden : "\(cInfo.chatViewName):"
|
||||
|
||||
+17
-15
@@ -81,19 +81,21 @@ The `ChatCommand` enum ([`AppAPITypes.swift` L15](../Shared/Model/AppAPITypes.sw
|
||||
| Command | Parameters | Description | Source |
|
||||
|---------|-----------|-------------|--------|
|
||||
| `apiGetChats` | `userId: Int64` | Get all chat previews for user | [L44](../Shared/Model/AppAPITypes.swift#L44) |
|
||||
| `apiGetChat` | `chatId, scope, contentTag, pagination, search` | Get messages for a chat | [L45](../Shared/Model/AppAPITypes.swift#L45) |
|
||||
| `apiGetChat` | `chatId, scope, contentTag, pagination, search, parentItemId` | Get messages for a chat. When `parentItemId` is non-nil, returns the comments thread under that channel post (mutually exclusive with `scope`). | [L45](../Shared/Model/AppAPITypes.swift#L45) |
|
||||
| `apiGetChatContentTypes` | `chatId, scope` | Get content type counts for a chat | [L46](../Shared/Model/AppAPITypes.swift#L46) |
|
||||
| `apiGetChatItemInfo` | `type, id, scope, itemId` | Get detailed info for a message | [L47](../Shared/Model/AppAPITypes.swift#L47) |
|
||||
| `apiSendMessages` | `type, id, scope, sendAsGroup, live, ttl, composedMessages` | Send one or more messages; `sendAsGroup` sends as channel owner | [L48](../Shared/Model/AppAPITypes.swift#L48) |
|
||||
| `apiCreateChatItems` | `noteFolderId, composedMessages` | Create items in notes folder | [L54](../Shared/Model/AppAPITypes.swift#L54) |
|
||||
| `apiUpdateChatItem` | `type, id, scope, itemId, updatedMessage, live` | Edit a sent message | [L56](../Shared/Model/AppAPITypes.swift#L56) |
|
||||
| `apiDeleteChatItem` | `type, id, scope, itemIds, mode` | Delete messages | [L57](../Shared/Model/AppAPITypes.swift#L57) |
|
||||
| `apiDeleteMemberChatItem` | `groupId, itemIds` | Moderate group messages | [L58](../Shared/Model/AppAPITypes.swift#L58) |
|
||||
| `apiChatItemReaction` | `type, id, scope, itemId, add, reaction` | Add/remove emoji reaction | [L61](../Shared/Model/AppAPITypes.swift#L61) |
|
||||
| `apiGetReactionMembers` | `userId, groupId, itemId, reaction` | Get who reacted | [L62](../Shared/Model/AppAPITypes.swift#L62) |
|
||||
| `apiPlanForwardChatItems` | `fromChatType, fromChatId, fromScope, itemIds` | Plan message forwarding | [L63](../Shared/Model/AppAPITypes.swift#L63) |
|
||||
| `apiForwardChatItems` | `toChatType, toChatId, toScope, sendAsGroup, from..., itemIds, ttl` | Forward messages; `sendAsGroup` forwards as channel owner | [L64](../Shared/Model/AppAPITypes.swift#L64) |
|
||||
| `apiReportMessage` | `groupId, chatItemId, reportReason, reportText` | Report group message | [L55](../Shared/Model/AppAPITypes.swift#L55) |
|
||||
| `apiSendComment` | `groupId, parentItemId, live, ttl, composedMessages` | Send one or more comments under a channel post. Maps to Haskell `/_comment #<groupId> <parentItemId>`; requires `useRelays' gInfo`. Outbound: only structural and content validation runs locally (quoted items must be in the same comment section). Authorization (role gate, `commentsDisabled` gate) is enforced at the receiver via the same XMsgNew/XMsgUpdate inbound gates that protect channel posts. | [L49](../Shared/Model/AppAPITypes.swift#L49) |
|
||||
| `apiSetCommentsDisabled` | `groupId, parentItemId, disabled` | Per-post comments override. Maps to Haskell `/_comments_disabled #<groupId> <parentItemId> on|off`; requires `useRelays' gInfo` and caller role at or above `GRModerator`. Returns `CRCmdOk` (the Swift wrapper is `async throws -> Void`). | [L50](../Shared/Model/AppAPITypes.swift#L50) |
|
||||
| `apiCreateChatItems` | `noteFolderId, composedMessages` | Create items in notes folder | [L56](../Shared/Model/AppAPITypes.swift#L56) |
|
||||
| `apiUpdateChatItem` | `type, id, scope, itemId, updatedMessage, live` | Edit a sent message | [L58](../Shared/Model/AppAPITypes.swift#L58) |
|
||||
| `apiDeleteChatItem` | `type, id, scope, itemIds, mode` | Delete messages | [L59](../Shared/Model/AppAPITypes.swift#L59) |
|
||||
| `apiDeleteMemberChatItem` | `groupId, itemIds` | Moderate group messages | [L60](../Shared/Model/AppAPITypes.swift#L60) |
|
||||
| `apiChatItemReaction` | `type, id, scope, itemId, add, reaction` | Add/remove emoji reaction (also valid on a comment item: reactions on comments are sent with `scope = nil` and reach all thread participants via the comment's `parent` chain) | [L63](../Shared/Model/AppAPITypes.swift#L63) |
|
||||
| `apiGetReactionMembers` | `userId, groupId, itemId, reaction` | Get who reacted | [L64](../Shared/Model/AppAPITypes.swift#L64) |
|
||||
| `apiPlanForwardChatItems` | `fromChatType, fromChatId, fromScope, itemIds` | Plan message forwarding | [L65](../Shared/Model/AppAPITypes.swift#L65) |
|
||||
| `apiForwardChatItems` | `toChatType, toChatId, toScope, sendAsGroup, from..., itemIds, ttl` | Forward messages; `sendAsGroup` forwards as channel owner | [L66](../Shared/Model/AppAPITypes.swift#L66) |
|
||||
| `apiReportMessage` | `groupId, chatItemId, reportReason, reportText` | Report group message | [L57](../Shared/Model/AppAPITypes.swift#L57) |
|
||||
| `apiChatRead` | `type, id, scope` | Mark entire chat as read | [L166](../Shared/Model/AppAPITypes.swift#L166) |
|
||||
| `apiChatItemsRead` | `type, id, scope, itemIds` | Mark specific items as read | [L167](../Shared/Model/AppAPITypes.swift#L167) |
|
||||
| `apiChatUnread` | `type, id, unreadChat` | Toggle unread badge | [L168](../Shared/Model/AppAPITypes.swift#L168) |
|
||||
@@ -154,11 +156,11 @@ The `ChatCommand` enum ([`AppAPITypes.swift` L15](../Shared/Model/AppAPITypes.sw
|
||||
| Command | Parameters | Description | Source |
|
||||
|---------|-----------|-------------|--------|
|
||||
| `apiGetChatTags` | `userId` | Get all user tags | [L43](../Shared/Model/AppAPITypes.swift#L43) |
|
||||
| `apiCreateChatTag` | `tag: ChatTagData` | Create a new tag | [L49](../Shared/Model/AppAPITypes.swift#L49) |
|
||||
| `apiSetChatTags` | `type, id, tagIds` | Assign tags to a chat | [L50](../Shared/Model/AppAPITypes.swift#L50) |
|
||||
| `apiDeleteChatTag` | `tagId` | Delete a tag | [L51](../Shared/Model/AppAPITypes.swift#L51) |
|
||||
| `apiUpdateChatTag` | `tagId, tagData` | Update tag name/emoji | [L52](../Shared/Model/AppAPITypes.swift#L52) |
|
||||
| `apiReorderChatTags` | `tagIds` | Reorder tags | [L53](../Shared/Model/AppAPITypes.swift#L53) |
|
||||
| `apiCreateChatTag` | `tag: ChatTagData` | Create a new tag | [L51](../Shared/Model/AppAPITypes.swift#L51) |
|
||||
| `apiSetChatTags` | `type, id, tagIds` | Assign tags to a chat | [L52](../Shared/Model/AppAPITypes.swift#L52) |
|
||||
| `apiDeleteChatTag` | `tagId` | Delete a tag | [L53](../Shared/Model/AppAPITypes.swift#L53) |
|
||||
| `apiUpdateChatTag` | `tagId, tagData` | Update tag name/emoji | [L54](../Shared/Model/AppAPITypes.swift#L54) |
|
||||
| `apiReorderChatTags` | `tagIds` | Reorder tags | [L55](../Shared/Model/AppAPITypes.swift#L55) |
|
||||
|
||||
### 2.7 File Operations
|
||||
|
||||
|
||||
@@ -46,14 +46,16 @@
|
||||
|
||||
## 1. Swift Source Impact
|
||||
|
||||
[GAP] `product/views/comments.md` is not yet defined. Channel-comments slice 3 (data model + API surface) landed in `ChatTypes.swift`, `ChatModel.swift`, `AppAPITypes.swift`, `SimpleXAPI.swift` and ~16 view files (pattern-match cascade for the third `ChatInfo.group` associated value). The view-layer product concept will be created in slice 5 — see channel-comments plan §5.4. Until then, treat the channel-comments rows below as affecting PC31 (Channels) only.
|
||||
|
||||
| Source File | Product Concepts Affected | Risk Level | Notes |
|
||||
|-------------|--------------------------|------------|-------|
|
||||
| Shared/ContentView.swift | PC1, PC2, PC3 | High | Root navigation — affects all chat access |
|
||||
| Shared/SimpleXApp.swift | PC1 through PC31 | High | App entry point — initialization affects everything |
|
||||
| Shared/AppDelegate.swift | PC18 | Medium | Push notification registration |
|
||||
| Shared/Views/ChatList/ChatListView.swift | PC1, PC28 | High | Main screen rendering and filtering |
|
||||
| Shared/Views/Chat/ChatView.swift | PC2, PC3, PC4, PC5, PC6, PC7, PC8, PC9, PC11, PC31 | High | Core conversation UI — most messaging features, channel message rendering |
|
||||
| Shared/Views/Chat/ComposeMessage/ComposeView.swift | PC4, PC6, PC9, PC11, PC31 | High | Message composition — send path for all messages, channel sendAsGroup |
|
||||
| Shared/Views/Chat/ChatView.swift | PC2, PC3, PC4, PC5, PC6, PC7, PC8, PC9, PC11, PC31 | High | Core conversation UI — most messaging features, channel message rendering. Slice 3: pattern-match cascade for `ChatInfo.group` third arg. [GAP] product/views/comments.md (slice 5). |
|
||||
| Shared/Views/Chat/ComposeMessage/ComposeView.swift | PC4, PC6, PC9, PC11, PC31 | High | Message composition — send path for all messages, channel sendAsGroup. Slice 3: pattern-match cascade for `ChatInfo.group` third arg (no compose logic changes yet — compose-as-comment lands in slice 4). [GAP] product/views/comments.md (slice 5). |
|
||||
| Shared/Views/Chat/ChatItem/ | PC2, PC3, PC5, PC7, PC8, PC9, PC10, PC11 | Medium | Individual message rendering components |
|
||||
| Shared/Views/Chat/ChatInfoView.swift | PC2, PC13, PC20 | Medium | Contact details and verification |
|
||||
| Shared/Views/Chat/Group/GroupChatInfoView.swift | PC3, PC14, PC15, PC16, PC30, PC31 | High | Group management hub, channel info adaptations |
|
||||
@@ -75,13 +77,13 @@
|
||||
| Shared/Views/LocalAuth/ | PC22 | Medium | App lock functionality |
|
||||
| Shared/Views/Database/ | PC23, PC26 | High | Database encryption and export |
|
||||
| Shared/Views/Migration/ | PC26 | High | Device migration — data portability |
|
||||
| Shared/Model/ChatModel.swift | PC1 through PC31 | High | Central state — all features depend on it |
|
||||
| Shared/Model/SimpleXAPI.swift | PC1 through PC31 | High | FFI bridge — all commands flow through here |
|
||||
| Shared/Model/AppAPITypes.swift | PC1 through PC31 | High | Command/response types — all API communication |
|
||||
| Shared/Model/ChatModel.swift | PC1 through PC31 | High | Central state — all features depend on it. Slice 3: added `SecondaryItemsModelFilter.groupChannelMsgContext`, `ItemsModel.loadSecondaryChat` comments branch with `injectChannelMsgInfo`, `getCIItemsModel` comments-thread branch, four `cInfo.channelMsgInfo() == nil` gating sites. [GAP] product/views/comments.md (slice 5). |
|
||||
| Shared/Model/SimpleXAPI.swift | PC1 through PC31 | High | FFI bridge — all commands flow through here. Slice 3: added `apiSendComment`, `apiSetCommentsDisabled`, `apiGetChat(parentItemId:)` extension. [GAP] product/views/comments.md (slice 5). |
|
||||
| Shared/Model/AppAPITypes.swift | PC1 through PC31 | High | Command/response types — all API communication. Slice 3: added `apiSendComment` and `apiSetCommentsDisabled` enum cases, `parentItemId` parameter on `apiGetChat`. [GAP] product/views/comments.md (slice 5). |
|
||||
| Shared/Model/NtfManager.swift | PC18 | High | Notification delivery |
|
||||
| Shared/Model/BGManager.swift | PC18 | Medium | Background fetch scheduling |
|
||||
| Shared/Theme/ThemeManager.swift | PC24 | Medium | Theme resolution engine |
|
||||
| SimpleXChat/ChatTypes.swift | PC1 through PC31 | High | Core data types — all features use them |
|
||||
| SimpleXChat/ChatTypes.swift | PC1 through PC31 | High | Core data types — all features use them. Slice 3: extended `ChatInfo.group` to three associated values (added `ChannelMsgInfo?` carrier), added `ChannelMsgInfo` struct, added `ChatItem.parentChatItemId`/`commentsTotal`/`commentsDisabled` (hoisted from `meta` on decode), added `CIMeta.itemSharedMsgId`. [GAP] product/views/comments.md (slice 5). |
|
||||
| SimpleXChat/APITypes.swift | PC1 through PC31 | High | API result types and error handling |
|
||||
| SimpleXChat/CallTypes.swift | PC17 | Medium | Call-specific data types |
|
||||
| SimpleXChat/FileUtils.swift | PC10, PC23, PC26 | Medium | File paths and encryption utilities |
|
||||
|
||||
+168
-120
@@ -1,6 +1,6 @@
|
||||
# SimpleX Chat iOS -- State Management
|
||||
|
||||
**Source:** [`ChatModel.swift`](../Shared/Model/ChatModel.swift#L1-L1404) | [`ChatTypes.swift`](../SimpleXChat/ChatTypes.swift#L1-L5377)
|
||||
**Source:** [`ChatModel.swift`](../Shared/Model/ChatModel.swift#L1-L1483) | [`ChatTypes.swift`](../SimpleXChat/ChatTypes.swift#L1-L5377)
|
||||
|
||||
> Technical specification for the app's state architecture: ChatModel, ItemsModel, Chat, ChatInfo, and preference storage.
|
||||
>
|
||||
@@ -63,151 +63,152 @@ ChatTagsModel (singleton -- filter state)
|
||||
|
||||
---
|
||||
|
||||
## 2. [ChatModel](../Shared/Model/ChatModel.swift#L353-L1289)
|
||||
## 2. [ChatModel](../Shared/Model/ChatModel.swift#L416-L1368)
|
||||
|
||||
**Class**: `final class ChatModel: ObservableObject`
|
||||
**Singleton**: `ChatModel.shared`
|
||||
**Source**: [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L353)
|
||||
**Source**: [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L416)
|
||||
|
||||
### Key Published Properties
|
||||
|
||||
#### App Lifecycle
|
||||
| Property | Type | Description | Line |
|
||||
|----------|------|-------------|------|
|
||||
| `onboardingStage` | `OnboardingStage?` | Current onboarding step | [L354](../Shared/Model/ChatModel.swift#L354) |
|
||||
| `chatInitialized` | `Bool` | Whether chat has been initialized | [L363](../Shared/Model/ChatModel.swift#L363) |
|
||||
| `chatRunning` | `Bool?` | Whether chat engine is running | [L364](../Shared/Model/ChatModel.swift#L364) |
|
||||
| `chatDbChanged` | `Bool` | Whether DB was changed externally | [L365](../Shared/Model/ChatModel.swift#L365) |
|
||||
| `chatDbEncrypted` | `Bool?` | Whether DB is encrypted | [L366](../Shared/Model/ChatModel.swift#L366) |
|
||||
| `chatDbStatus` | `DBMigrationResult?` | DB migration status | [L367](../Shared/Model/ChatModel.swift#L367) |
|
||||
| `ctrlInitInProgress` | `Bool` | Whether controller is initializing | [L368](../Shared/Model/ChatModel.swift#L368) |
|
||||
| `migrationState` | `MigrationToState?` | Device migration state | [L417](../Shared/Model/ChatModel.swift#L417) |
|
||||
| `onboardingStage` | `OnboardingStage?` | Current onboarding step | [L417](../Shared/Model/ChatModel.swift#L417) |
|
||||
| `chatInitialized` | `Bool` | Whether chat has been initialized | [L426](../Shared/Model/ChatModel.swift#L426) |
|
||||
| `chatRunning` | `Bool?` | Whether chat engine is running | [L427](../Shared/Model/ChatModel.swift#L427) |
|
||||
| `chatDbChanged` | `Bool` | Whether DB was changed externally | [L428](../Shared/Model/ChatModel.swift#L428) |
|
||||
| `chatDbEncrypted` | `Bool?` | Whether DB is encrypted | [L429](../Shared/Model/ChatModel.swift#L429) |
|
||||
| `chatDbStatus` | `DBMigrationResult?` | DB migration status | [L430](../Shared/Model/ChatModel.swift#L430) |
|
||||
| `ctrlInitInProgress` | `Bool` | Whether controller is initializing | [L431](../Shared/Model/ChatModel.swift#L431) |
|
||||
| `migrationState` | `MigrationToState?` | Device migration state | [L481](../Shared/Model/ChatModel.swift#L481) |
|
||||
|
||||
#### User State
|
||||
| Property | Type | Description | Line |
|
||||
|----------|------|-------------|------|
|
||||
| `currentUser` | `User?` | Active user profile (triggers theme reapply on change) | [L357](../Shared/Model/ChatModel.swift#L357) |
|
||||
| `users` | `[UserInfo]` | All user profiles | [L362](../Shared/Model/ChatModel.swift#L362) |
|
||||
| `v3DBMigration` | `V3DBMigrationState` | Legacy DB migration state | [L356](../Shared/Model/ChatModel.swift#L356) |
|
||||
| `currentUser` | `User?` | Active user profile (triggers theme reapply on change) | [L420](../Shared/Model/ChatModel.swift#L420) |
|
||||
| `users` | `[UserInfo]` | All user profiles | [L425](../Shared/Model/ChatModel.swift#L425) |
|
||||
| `v3DBMigration` | `V3DBMigrationState` | Legacy DB migration state | [L419](../Shared/Model/ChatModel.swift#L419) |
|
||||
|
||||
#### Chat List
|
||||
| Property | Type | Description | Line |
|
||||
|----------|------|-------------|------|
|
||||
| `chats` | `[Chat]` (private set) | All conversations for current user | [L374](../Shared/Model/ChatModel.swift#L374) |
|
||||
| `deletedChats` | `Set<String>` | Chat IDs pending deletion animation | [L375](../Shared/Model/ChatModel.swift#L375) |
|
||||
| `chats` | `[Chat]` (private set) | All conversations for current user | [L437](../Shared/Model/ChatModel.swift#L437) |
|
||||
| `deletedChats` | `Set<String>` | Chat IDs pending deletion animation | [L438](../Shared/Model/ChatModel.swift#L438) |
|
||||
|
||||
#### Active Chat
|
||||
| Property | Type | Description | Line |
|
||||
|----------|------|-------------|------|
|
||||
| `chatId` | `String?` | Currently open chat ID | [L377](../Shared/Model/ChatModel.swift#L377) |
|
||||
| `chatAgentConnId` | `String?` | Agent connection ID for active chat | [L378](../Shared/Model/ChatModel.swift#L378) |
|
||||
| `chatSubStatus` | `SubscriptionStatus?` | Active chat subscription status | [L379](../Shared/Model/ChatModel.swift#L379) |
|
||||
| `openAroundItemId` | `ChatItem.ID?` | Item to scroll to when opening | [L380](../Shared/Model/ChatModel.swift#L380) |
|
||||
| `chatToTop` | `String?` | Chat to scroll to top | [L381](../Shared/Model/ChatModel.swift#L381) |
|
||||
| `groupMembers` | `[GMember]` | Members of active group | [L382](../Shared/Model/ChatModel.swift#L382) |
|
||||
| `groupMembersIndexes` | `[Int64: Int]` | Member ID to index mapping | [L383](../Shared/Model/ChatModel.swift#L383) |
|
||||
| `membersLoaded` | `Bool` | Whether members have been loaded | [L384](../Shared/Model/ChatModel.swift#L384) |
|
||||
| `secondaryIM` | `ItemsModel?` | Secondary items model (e.g. support chat scope) | [L435](../Shared/Model/ChatModel.swift#L435) |
|
||||
| `chatId` | `String?` | Currently open chat ID | [L440](../Shared/Model/ChatModel.swift#L440) |
|
||||
| `chatAgentConnId` | `String?` | Agent connection ID for active chat | [L441](../Shared/Model/ChatModel.swift#L441) |
|
||||
| `chatSubStatus` | `SubscriptionStatus?` | Active chat subscription status | [L442](../Shared/Model/ChatModel.swift#L442) |
|
||||
| `openAroundItemId` | `ChatItem.ID?` | Item to scroll to when opening | [L443](../Shared/Model/ChatModel.swift#L443) |
|
||||
| `chatToTop` | `String?` | Chat to scroll to top | [L444](../Shared/Model/ChatModel.swift#L444) |
|
||||
| `groupMembers` | `[GMember]` | Members of active group | [L446](../Shared/Model/ChatModel.swift#L446) |
|
||||
| `groupMembersIndexes` | `[Int64: Int]` | Member ID to index mapping | [L447](../Shared/Model/ChatModel.swift#L447) |
|
||||
| `membersLoaded` | `Bool` | Whether members have been loaded | [L448](../Shared/Model/ChatModel.swift#L448) |
|
||||
| `secondaryIM` | `ItemsModel?` | Secondary items model (member support scope, channel comments thread, or content-type filter) | [L499](../Shared/Model/ChatModel.swift#L499) |
|
||||
|
||||
#### Authentication
|
||||
| Property | Type | Description | Line |
|
||||
|----------|------|-------------|------|
|
||||
| `contentViewAccessAuthenticated` | `Bool` | Whether user has passed authentication | [L371](../Shared/Model/ChatModel.swift#L371) |
|
||||
| `laRequest` | `LocalAuthRequest?` | Pending authentication request | [L372](../Shared/Model/ChatModel.swift#L372) |
|
||||
| `contentViewAccessAuthenticated` | `Bool` | Whether user has passed authentication | [L434](../Shared/Model/ChatModel.swift#L434) |
|
||||
| `laRequest` | `LocalAuthRequest?` | Pending authentication request | [L435](../Shared/Model/ChatModel.swift#L435) |
|
||||
|
||||
#### Notifications
|
||||
| Property | Type | Description | Line |
|
||||
|----------|------|-------------|------|
|
||||
| `deviceToken` | `DeviceToken?` | Current APNs device token | [L395](../Shared/Model/ChatModel.swift#L395) |
|
||||
| `savedToken` | `DeviceToken?` | Previously saved token | [L396](../Shared/Model/ChatModel.swift#L396) |
|
||||
| `tokenRegistered` | `Bool` | Whether token is registered with server | [L397](../Shared/Model/ChatModel.swift#L397) |
|
||||
| `tokenStatus` | `NtfTknStatus?` | Token registration status | [L399](../Shared/Model/ChatModel.swift#L399) |
|
||||
| `notificationMode` | `NotificationsMode` | Current notification mode (.off/.periodic/.instant) | [L400](../Shared/Model/ChatModel.swift#L400) |
|
||||
| `notificationServer` | `String?` | Notification server URL | [L401](../Shared/Model/ChatModel.swift#L401) |
|
||||
| `notificationPreview` | `NotificationPreviewMode` | What to show in notifications | [L402](../Shared/Model/ChatModel.swift#L402) |
|
||||
| `notificationResponse` | `UNNotificationResponse?` | Pending notification action | [L369](../Shared/Model/ChatModel.swift#L369) |
|
||||
| `ntfContactRequest` | `NTFContactRequest?` | Pending contact request from notification | [L404](../Shared/Model/ChatModel.swift#L404) |
|
||||
| `ntfCallInvitationAction` | `(ChatId, NtfCallAction)?` | Pending call action from notification | [L405](../Shared/Model/ChatModel.swift#L405) |
|
||||
| `deviceToken` | `DeviceToken?` | Current APNs device token | [L459](../Shared/Model/ChatModel.swift#L459) |
|
||||
| `savedToken` | `DeviceToken?` | Previously saved token | [L460](../Shared/Model/ChatModel.swift#L460) |
|
||||
| `tokenRegistered` | `Bool` | Whether token is registered with server | [L461](../Shared/Model/ChatModel.swift#L461) |
|
||||
| `tokenStatus` | `NtfTknStatus?` | Token registration status | [L463](../Shared/Model/ChatModel.swift#L463) |
|
||||
| `notificationMode` | `NotificationsMode` | Current notification mode (.off/.periodic/.instant) | [L464](../Shared/Model/ChatModel.swift#L464) |
|
||||
| `notificationServer` | `String?` | Notification server URL | [L465](../Shared/Model/ChatModel.swift#L465) |
|
||||
| `notificationPreview` | `NotificationPreviewMode` | What to show in notifications | [L466](../Shared/Model/ChatModel.swift#L466) |
|
||||
| `notificationResponse` | `UNNotificationResponse?` | Pending notification action | [L432](../Shared/Model/ChatModel.swift#L432) |
|
||||
| `ntfContactRequest` | `NTFContactRequest?` | Pending contact request from notification | [L468](../Shared/Model/ChatModel.swift#L468) |
|
||||
| `ntfCallInvitationAction` | `(ChatId, NtfCallAction)?` | Pending call action from notification | [L469](../Shared/Model/ChatModel.swift#L469) |
|
||||
|
||||
#### Calls
|
||||
| Property | Type | Description | Line |
|
||||
|----------|------|-------------|------|
|
||||
| `callInvitations` | `[ChatId: RcvCallInvitation]` | Pending incoming call invitations | [L407](../Shared/Model/ChatModel.swift#L407) |
|
||||
| `activeCall` | `Call?` | Currently active call | [L408](../Shared/Model/ChatModel.swift#L408) |
|
||||
| `callCommand` | `WebRTCCommandProcessor` | WebRTC command queue | [L409](../Shared/Model/ChatModel.swift#L409) |
|
||||
| `showCallView` | `Bool` | Whether to show full-screen call UI | [L410](../Shared/Model/ChatModel.swift#L410) |
|
||||
| `activeCallViewIsCollapsed` | `Bool` | Whether call view is in PiP mode | [L411](../Shared/Model/ChatModel.swift#L411) |
|
||||
| `callInvitations` | `[ChatId: RcvCallInvitation]` | Pending incoming call invitations | [L471](../Shared/Model/ChatModel.swift#L471) |
|
||||
| `activeCall` | `Call?` | Currently active call | [L472](../Shared/Model/ChatModel.swift#L472) |
|
||||
| `callCommand` | `WebRTCCommandProcessor` | WebRTC command queue | [L473](../Shared/Model/ChatModel.swift#L473) |
|
||||
| `showCallView` | `Bool` | Whether to show full-screen call UI | [L474](../Shared/Model/ChatModel.swift#L474) |
|
||||
| `activeCallViewIsCollapsed` | `Bool` | Whether call view is in PiP mode | [L475](../Shared/Model/ChatModel.swift#L475) |
|
||||
|
||||
#### Remote Desktop
|
||||
| Property | Type | Description | Line |
|
||||
|----------|------|-------------|------|
|
||||
| `remoteCtrlSession` | `RemoteCtrlSession?` | Active remote desktop session | [L414](../Shared/Model/ChatModel.swift#L414) |
|
||||
| `remoteCtrlSession` | `RemoteCtrlSession?` | Active remote desktop session | [L478](../Shared/Model/ChatModel.swift#L478) |
|
||||
|
||||
#### Misc
|
||||
| Property | Type | Description | Line |
|
||||
|----------|------|-------------|------|
|
||||
| `userAddress` | `UserContactLink?` | User's SimpleX address | [L391](../Shared/Model/ChatModel.swift#L391) |
|
||||
| `chatItemTTL` | `ChatItemTTL` | Global message TTL | [L392](../Shared/Model/ChatModel.swift#L392) |
|
||||
| `appOpenUrl` | `URL?` | URL opened while app active | [L393](../Shared/Model/ChatModel.swift#L393) |
|
||||
| `appOpenUrlLater` | `URL?` | URL opened while app inactive | [L394](../Shared/Model/ChatModel.swift#L394) |
|
||||
| `showingInvitation` | `ShowingInvitation?` | Currently displayed invitation | [L416](../Shared/Model/ChatModel.swift#L416) |
|
||||
| `draft` | `ComposeState?` | Saved compose draft | [L420](../Shared/Model/ChatModel.swift#L420) |
|
||||
| `draftChatId` | `String?` | Chat ID for saved draft | [L421](../Shared/Model/ChatModel.swift#L421) |
|
||||
| `networkInfo` | `UserNetworkInfo` | Current network type and status | [L422](../Shared/Model/ChatModel.swift#L422) |
|
||||
| `conditions` | `ServerOperatorConditions` | Server usage conditions | [L424](../Shared/Model/ChatModel.swift#L424) |
|
||||
| `stopPreviousRecPlay` | `URL?` | Currently playing audio source | [L419](../Shared/Model/ChatModel.swift#L419) |
|
||||
| `userAddress` | `UserContactLink?` | User's SimpleX address | [L455](../Shared/Model/ChatModel.swift#L455) |
|
||||
| `chatItemTTL` | `ChatItemTTL` | Global message TTL | [L456](../Shared/Model/ChatModel.swift#L456) |
|
||||
| `appOpenUrl` | `URL?` | URL opened while app active | [L457](../Shared/Model/ChatModel.swift#L457) |
|
||||
| `appOpenUrlLater` | `URL?` | URL opened while app inactive | [L458](../Shared/Model/ChatModel.swift#L458) |
|
||||
| `showingInvitation` | `ShowingInvitation?` | Currently displayed invitation | [L480](../Shared/Model/ChatModel.swift#L480) |
|
||||
| `draft` | `ComposeState?` | Saved compose draft | [L484](../Shared/Model/ChatModel.swift#L484) |
|
||||
| `draftChatId` | `String?` | Chat ID for saved draft | [L485](../Shared/Model/ChatModel.swift#L485) |
|
||||
| `networkInfo` | `UserNetworkInfo` | Current network type and status | [L486](../Shared/Model/ChatModel.swift#L486) |
|
||||
| `conditions` | `ServerOperatorConditions` | Server usage conditions | [L488](../Shared/Model/ChatModel.swift#L488) |
|
||||
| `stopPreviousRecPlay` | `URL?` | Currently playing audio source | [L483](../Shared/Model/ChatModel.swift#L483) |
|
||||
|
||||
### Non-Published Properties
|
||||
|
||||
| Property | Type | Description | Line |
|
||||
|----------|------|-------------|------|
|
||||
| `messageDelivery` | `[Int64: () -> Void]` | Pending delivery confirmation callbacks | [L426](../Shared/Model/ChatModel.swift#L426) |
|
||||
| `filesToDelete` | `Set<URL>` | Files queued for deletion | [L428](../Shared/Model/ChatModel.swift#L428) |
|
||||
| `im` | `ItemsModel` | Reference to `ItemsModel.shared` | [L432](../Shared/Model/ChatModel.swift#L432) |
|
||||
| `messageDelivery` | `[Int64: () -> Void]` | Pending delivery confirmation callbacks | [L490](../Shared/Model/ChatModel.swift#L490) |
|
||||
| `filesToDelete` | `Set<URL>` | Files queued for deletion | [L492](../Shared/Model/ChatModel.swift#L492) |
|
||||
| `im` | `ItemsModel` | Reference to `ItemsModel.shared` | [L496](../Shared/Model/ChatModel.swift#L496) |
|
||||
|
||||
### Key Methods
|
||||
|
||||
| Method | Description | Line |
|
||||
|--------|-------------|------|
|
||||
| `getUser(_ userId:)` | Find user by ID | [L455](../Shared/Model/ChatModel.swift#L455) |
|
||||
| `updateUser(_ user:)` | Update user in list and current | [L466](../Shared/Model/ChatModel.swift#L466) |
|
||||
| `removeUser(_ user:)` | Remove user from list | [L476](../Shared/Model/ChatModel.swift#L476) |
|
||||
| `getChat(_ id:)` | Find chat by ID | [L487](../Shared/Model/ChatModel.swift#L487) |
|
||||
| `addChat(_ chat:)` | Add chat to list | [L542](../Shared/Model/ChatModel.swift#L542) |
|
||||
| `updateChatInfo(_ cInfo:)` | Update chat metadata | [L556](../Shared/Model/ChatModel.swift#L556) |
|
||||
| `replaceChat(_ id:, _ chat:)` | Replace chat in list | [L608](../Shared/Model/ChatModel.swift#L608) |
|
||||
| `removeChat(_ id:)` | Remove chat from list | [L1217](../Shared/Model/ChatModel.swift#L1217) |
|
||||
| `popChat(_ id:)` | Move chat to top of list | [L1193](../Shared/Model/ChatModel.swift#L1193) |
|
||||
| `totalUnreadCountForAllUsers()` | Sum unread across all users | [L1093](../Shared/Model/ChatModel.swift#L1093) |
|
||||
| `getUser(_ userId:)` | Find user by ID | [L519](../Shared/Model/ChatModel.swift#L519) |
|
||||
| `updateUser(_ user:)` | Update user in list and current | [L530](../Shared/Model/ChatModel.swift#L530) |
|
||||
| `removeUser(_ user:)` | Remove user from list | [L540](../Shared/Model/ChatModel.swift#L540) |
|
||||
| `getChat(_ id:)` | Find chat by ID | [L551](../Shared/Model/ChatModel.swift#L551) |
|
||||
| `addChat(_ chat:)` | Add chat to list | [L606](../Shared/Model/ChatModel.swift#L606) |
|
||||
| `updateChatInfo(_ cInfo:)` | Update chat metadata | [L620](../Shared/Model/ChatModel.swift#L620) |
|
||||
| `replaceChat(_ id:, _ chat:)` | Replace chat in list | [L672](../Shared/Model/ChatModel.swift#L672) |
|
||||
| `getCIItemsModel(_ cInfo:_ ci:)` | Resolve which `ItemsModel` an incoming item belongs to (primary `im`, secondary `secondaryIM`, or `nil`). For the channel-comments thread it routes via `ci.parentChatItemId == parent.id`. | [L747](../Shared/Model/ChatModel.swift#L747) |
|
||||
| `removeChat(_ id:)` | Remove chat from list | [L1290](../Shared/Model/ChatModel.swift#L1290) |
|
||||
| `popChat(_ id:)` | Move chat to top of list | [L1266](../Shared/Model/ChatModel.swift#L1266) |
|
||||
| `totalUnreadCountForAllUsers()` | Sum unread across all users | [L1166](../Shared/Model/ChatModel.swift#L1166) |
|
||||
|
||||
---
|
||||
|
||||
## 3. [ItemsModel](../Shared/Model/ChatModel.swift#L74-L174)
|
||||
## 3. [ItemsModel](../Shared/Model/ChatModel.swift#L80-L227)
|
||||
|
||||
**Class**: `class ItemsModel: ObservableObject`
|
||||
**Primary singleton**: `ItemsModel.shared`
|
||||
**Secondary instances**: Created via `ItemsModel.loadSecondaryChat()` for scope-based views (e.g., group member support chat)
|
||||
**Source**: [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L74)
|
||||
**Secondary instances**: Created via `ItemsModel.loadSecondaryChat()` for scope-based views (member support chat, channel comments thread, reports filter).
|
||||
**Source**: [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L80)
|
||||
|
||||
### Properties
|
||||
|
||||
| Property | Type | Description | Line |
|
||||
|----------|------|-------------|------|
|
||||
| `reversedChatItems` | `[ChatItem]` | Messages in reverse chronological order (newest first) | [L80](../Shared/Model/ChatModel.swift#L80) |
|
||||
| `itemAdded` | `Bool` | Flag indicating a new item was added | [L83](../Shared/Model/ChatModel.swift#L83) |
|
||||
| `chatState` | `ActiveChatState` | Pagination splits and loaded ranges | [L87](../Shared/Model/ChatModel.swift#L87) |
|
||||
| `isLoading` | `Bool` | Whether messages are currently loading | [L91](../Shared/Model/ChatModel.swift#L91) |
|
||||
| `showLoadingProgress` | `ChatId?` | Chat ID showing loading spinner | [L92](../Shared/Model/ChatModel.swift#L92) |
|
||||
| `preloadState` | `PreloadState` | State for infinite-scroll preloading | [L77](../Shared/Model/ChatModel.swift#L77) |
|
||||
| `secondaryIMFilter` | `SecondaryItemsModelFilter?` | Filter for secondary instances | [L76](../Shared/Model/ChatModel.swift#L76) |
|
||||
| `reversedChatItems` | `[ChatItem]` | Messages in reverse chronological order (newest first) | [L86](../Shared/Model/ChatModel.swift#L86) |
|
||||
| `itemAdded` | `Bool` | Flag indicating a new item was added | [L89](../Shared/Model/ChatModel.swift#L89) |
|
||||
| `chatState` | `ActiveChatState` | Pagination splits and loaded ranges | [L93](../Shared/Model/ChatModel.swift#L93) |
|
||||
| `isLoading` | `Bool` | Whether messages are currently loading | [L97](../Shared/Model/ChatModel.swift#L97) |
|
||||
| `showLoadingProgress` | `ChatId?` | Chat ID showing loading spinner | [L98](../Shared/Model/ChatModel.swift#L98) |
|
||||
| `preloadState` | `PreloadState` | State for infinite-scroll preloading | [L83](../Shared/Model/ChatModel.swift#L83) |
|
||||
| `secondaryIMFilter` | `SecondaryItemsModelFilter?` | Filter for secondary instances | [L82](../Shared/Model/ChatModel.swift#L82) |
|
||||
|
||||
### Computed Properties
|
||||
|
||||
| Property | Type | Description | Line |
|
||||
|----------|------|-------------|------|
|
||||
| `lastItemsLoaded` | `Bool` | Whether the oldest messages have been loaded | [L97](../Shared/Model/ChatModel.swift#L97) |
|
||||
| `contentTag` | `MsgContentTag?` | Content type filter (if secondary) | [L159](../Shared/Model/ChatModel.swift#L159) |
|
||||
| `groupScopeInfo` | `GroupChatScopeInfo?` | Group scope filter (if secondary) | [L167](../Shared/Model/ChatModel.swift#L167) |
|
||||
| `lastItemsLoaded` | `Bool` | Whether the oldest messages have been loaded | [L103](../Shared/Model/ChatModel.swift#L103) |
|
||||
| `contentTag` | `MsgContentTag?` | Content type filter (if secondary) | [L212](../Shared/Model/ChatModel.swift#L212) |
|
||||
| `groupScopeInfo` | `GroupChatScopeInfo?` | Group scope filter (if secondary) | [L220](../Shared/Model/ChatModel.swift#L220) |
|
||||
|
||||
### Throttling
|
||||
|
||||
@@ -226,37 +227,52 @@ Direct `@Published` properties (`isLoading`, `showLoadingProgress`) bypass throt
|
||||
|
||||
| Method | Description | Line |
|
||||
|--------|-------------|------|
|
||||
| `loadOpenChat(_ chatId:)` | Load chat with 250ms navigation delay | [L117](../Shared/Model/ChatModel.swift#L117) |
|
||||
| `loadOpenChatNoWait(_ chatId:, _ openAroundItemId:)` | Load chat without delay | [L143](../Shared/Model/ChatModel.swift#L143) |
|
||||
| `loadSecondaryChat(_ chatId:, chatFilter:)` | Create secondary ItemsModel instance | [L110](../Shared/Model/ChatModel.swift#L110) |
|
||||
| `loadOpenChat(_ chatId:)` | Load chat with 250ms navigation delay | [L170](../Shared/Model/ChatModel.swift#L170) |
|
||||
| `loadOpenChatNoWait(_ chatId:, _ openAroundItemId:)` | Load chat without delay | [L196](../Shared/Model/ChatModel.swift#L196) |
|
||||
| `loadSecondaryChat(_ chatId:, chatFilter:)` | Create secondary `ItemsModel` instance. For `.groupChannelMsgContext` it fetches via `apiGetChat(parentItemId:)` and injects a local-only `ChannelMsgInfo` into the returned `ChatInfo.group`. | [L116](../Shared/Model/ChatModel.swift#L116) |
|
||||
|
||||
### [SecondaryItemsModelFilter](../Shared/Model/ChatModel.swift#L58-L70)
|
||||
### [SecondaryItemsModelFilter](../Shared/Model/ChatModel.swift#L58-L76)
|
||||
|
||||
Used for secondary chat views (e.g., group member support scope, content type filter):
|
||||
Used for secondary chat views (e.g., group member support scope, content type filter, channel comments thread):
|
||||
|
||||
```swift
|
||||
enum SecondaryItemsModelFilter {
|
||||
case groupChatScopeContext(groupScopeInfo: GroupChatScopeInfo)
|
||||
case msgContentTagContext(contentTag: MsgContentTag)
|
||||
case groupChannelMsgContext(parent: ChatItem)
|
||||
}
|
||||
```
|
||||
|
||||
`.groupChannelMsgContext` is the channel-comments-thread scope. The associated `parent` carries the channel post being commented on; the comments-thread view routes inbound items via `ci.parentChatItemId == parent.id`. The parent's `meta.itemSharedMsgId` is required before opening the thread (the owner-side parent may briefly lack a shared id during send).
|
||||
|
||||
### Channel comments thread
|
||||
|
||||
Opening a comments thread bypasses the standard `loadOpenChat` flow because the comments scope is not represented on the wire as a `GroupChatScope`. Instead:
|
||||
|
||||
1. The caller invokes [`ItemsModel.loadSecondaryChat`](../Shared/Model/ChatModel.swift#L116-L153) with `chatFilter: .groupChannelMsgContext(parent:)`.
|
||||
2. Items are fetched via `apiGetChat(..., parentItemId: parent.id)` (see `apiGetChat` in [spec/api.md §2.3](api.md#23-chat--message-operations)).
|
||||
3. The returned `ChatInfo.group` has its third associated value rewritten by [`injectChannelMsgInfo`](../Shared/Model/ChatModel.swift#L158-L167) to embed a local-only [`ChannelMsgInfo`](../SimpleXChat/ChatTypes.swift#L2028-L2036) carrier so toolbar and routing logic can read the parent without a second lookup.
|
||||
4. [`getCIItemsModel`](../Shared/Model/ChatModel.swift#L747-L777) gains a new branch (`cInfo.channelMsgInfo() != nil`) that routes events into the secondary `ItemsModel` when `ci.parentChatItemId == parent.id`.
|
||||
5. Gating sites that previously checked `cInfo.groupChatScope() == nil` to decide whether to update the main chat preview now also check `cInfo.channelMsgInfo() == nil`, so comment items do not bubble into the main chat list.
|
||||
|
||||
The carrier is **not** serialized: `ChannelMsgInfo` is `Decodable` only so it nests cleanly into `ChatInfo.group`, but the server never sends this field — it is injected exclusively by `loadSecondaryChat`. Inbound `XMsgNew` items with a `parent` come back over the regular event stream; they are routed by `ci.parentChatItemId` alone.
|
||||
|
||||
---
|
||||
|
||||
## 4. [ChatTagsModel](../Shared/Model/ChatModel.swift#L189-L291)
|
||||
## 4. [ChatTagsModel](../Shared/Model/ChatModel.swift#L242-L344)
|
||||
|
||||
**Class**: `class ChatTagsModel: ObservableObject`
|
||||
**Singleton**: `ChatTagsModel.shared`
|
||||
**Source**: [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L189)
|
||||
**Source**: [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L242)
|
||||
|
||||
### Properties
|
||||
|
||||
| Property | Type | Description | Line |
|
||||
|----------|------|-------------|------|
|
||||
| `userTags` | `[ChatTag]` | User-defined tags | [L192](../Shared/Model/ChatModel.swift#L192) |
|
||||
| `activeFilter` | `ActiveFilter?` | Currently active filter tab | [L193](../Shared/Model/ChatModel.swift#L193) |
|
||||
| `presetTags` | `[PresetTag: Int]` | Preset tag counts (groups, contacts, favorites, etc.) | [L194](../Shared/Model/ChatModel.swift#L194) |
|
||||
| `unreadTags` | `[Int64: Int]` | Unread count per user tag | [L195](../Shared/Model/ChatModel.swift#L195) |
|
||||
| `userTags` | `[ChatTag]` | User-defined tags | [L245](../Shared/Model/ChatModel.swift#L245) |
|
||||
| `activeFilter` | `ActiveFilter?` | Currently active filter tab | [L246](../Shared/Model/ChatModel.swift#L246) |
|
||||
| `presetTags` | `[PresetTag: Int]` | Preset tag counts (groups, contacts, favorites, etc.) | [L247](../Shared/Model/ChatModel.swift#L247) |
|
||||
| `unreadTags` | `[Int64: Int]` | Unread count per user tag | [L248](../Shared/Model/ChatModel.swift#L248) |
|
||||
|
||||
### [ActiveFilter](../Shared/Views/ChatList/ChatListView.swift#L52)
|
||||
|
||||
@@ -270,11 +286,11 @@ enum ActiveFilter {
|
||||
|
||||
---
|
||||
|
||||
## 5. [ChannelRelaysModel](../Shared/Model/ChatModel.swift#L336-L350)
|
||||
## 5. [ChannelRelaysModel](../Shared/Model/ChatModel.swift#L389-L413)
|
||||
|
||||
**Class**: `class ChannelRelaysModel: ObservableObject`
|
||||
**Singleton**: `ChannelRelaysModel.shared`
|
||||
**Source**: [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L336)
|
||||
**Source**: [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L389)
|
||||
|
||||
Holds runtime relay state for the currently viewed channel. Used by `ChannelRelaysView` to display and manage relays. Reset when the view is dismissed.
|
||||
|
||||
@@ -282,22 +298,22 @@ Holds runtime relay state for the currently viewed channel. Used by `ChannelRela
|
||||
|
||||
| Property | Type | Description | Line |
|
||||
|----------|------|-------------|------|
|
||||
| `groupId` | `Int64?` | Group ID of the channel whose relays are loaded | [L338](../Shared/Model/ChatModel.swift#L338) |
|
||||
| `groupRelays` | `[GroupRelay]` | Current relay instances for the channel | [L339](../Shared/Model/ChatModel.swift#L339) |
|
||||
| `groupId` | `Int64?` | Group ID of the channel whose relays are loaded | [L391](../Shared/Model/ChatModel.swift#L391) |
|
||||
| `groupRelays` | `[GroupRelay]` | Current relay instances for the channel | [L392](../Shared/Model/ChatModel.swift#L392) |
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Description | Line |
|
||||
|--------|-------------|------|
|
||||
| `set(groupId:groupRelays:)` | Populate all properties at once | [L341](../Shared/Model/ChatModel.swift#L341) |
|
||||
| `reset()` | Clear all properties to nil/empty | [L346](../Shared/Model/ChatModel.swift#L346) |
|
||||
| `set(groupId:groupRelays:)` | Populate all properties at once | [L394](../Shared/Model/ChatModel.swift#L394) |
|
||||
| `reset()` | Clear all properties to nil/empty | [L409](../Shared/Model/ChatModel.swift#L409) |
|
||||
|
||||
---
|
||||
|
||||
## 6. [Chat](../Shared/Model/ChatModel.swift#L1301-L1353)
|
||||
## 6. [Chat](../Shared/Model/ChatModel.swift#L1380-L1432)
|
||||
|
||||
**Class**: `final class Chat: ObservableObject, Identifiable, ChatLike`
|
||||
**Source**: [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L1301)
|
||||
**Source**: [`Shared/Model/ChatModel.swift`](../Shared/Model/ChatModel.swift#L1380)
|
||||
|
||||
Represents a single conversation in the chat list. Each `Chat` is an independent observable object.
|
||||
|
||||
@@ -305,12 +321,12 @@ Represents a single conversation in the chat list. Each `Chat` is an independent
|
||||
|
||||
| Property | Type | Description | Line |
|
||||
|----------|------|-------------|------|
|
||||
| `chatInfo` | `ChatInfo` | Conversation type and metadata | [L1302](../Shared/Model/ChatModel.swift#L1302) |
|
||||
| `chatItems` | `[ChatItem]` | Preview items (typically last message) | [L1303](../Shared/Model/ChatModel.swift#L1303) |
|
||||
| `chatStats` | `ChatStats` | Unread counts and min unread item ID | [L1304](../Shared/Model/ChatModel.swift#L1304) |
|
||||
| `created` | `Date` | Creation timestamp | [L1305](../Shared/Model/ChatModel.swift#L1305) |
|
||||
| `chatInfo` | `ChatInfo` | Conversation type and metadata | [L1381](../Shared/Model/ChatModel.swift#L1381) |
|
||||
| `chatItems` | `[ChatItem]` | Preview items (typically last message) | [L1382](../Shared/Model/ChatModel.swift#L1382) |
|
||||
| `chatStats` | `ChatStats` | Unread counts and min unread item ID | [L1383](../Shared/Model/ChatModel.swift#L1383) |
|
||||
| `created` | `Date` | Creation timestamp | [L1384](../Shared/Model/ChatModel.swift#L1384) |
|
||||
|
||||
### [ChatStats](../SimpleXChat/ChatTypes.swift#L1881-L1903)
|
||||
### [ChatStats](../SimpleXChat/ChatTypes.swift#L1966-L1988)
|
||||
|
||||
```swift
|
||||
struct ChatStats: Decodable, Hashable {
|
||||
@@ -326,24 +342,24 @@ struct ChatStats: Decodable, Hashable {
|
||||
|
||||
| Property | Description | Line |
|
||||
|----------|-------------|------|
|
||||
| `id` | Chat ID from `chatInfo.id` | [L1336](../Shared/Model/ChatModel.swift#L1336) |
|
||||
| `viewId` | Unique view identity including creation time | [L1338](../Shared/Model/ChatModel.swift#L1338) |
|
||||
| `unreadTag` | Whether chat counts as "unread" based on notification settings | [L1328](../Shared/Model/ChatModel.swift#L1328) |
|
||||
| `supportUnreadCount` | Unread count for group support scope | [L1340](../Shared/Model/ChatModel.swift#L1340) |
|
||||
| `id` | Chat ID from `chatInfo.id` | [L1415](../Shared/Model/ChatModel.swift#L1415) |
|
||||
| `viewId` | Unique view identity including creation time | [L1417](../Shared/Model/ChatModel.swift#L1417) |
|
||||
| `unreadTag` | Whether chat counts as "unread" based on notification settings | [L1407](../Shared/Model/ChatModel.swift#L1407) |
|
||||
| `supportUnreadCount` | Unread count for group support scope | [L1419](../Shared/Model/ChatModel.swift#L1419) |
|
||||
|
||||
---
|
||||
|
||||
## 7. [ChatInfo](../SimpleXChat/ChatTypes.swift#L1374-L1856)
|
||||
## 7. [ChatInfo](../SimpleXChat/ChatTypes.swift#L1435-L1941)
|
||||
|
||||
**Enum**: `public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable`
|
||||
**Source**: [`SimpleXChat/ChatTypes.swift`](../SimpleXChat/ChatTypes.swift#L1374)
|
||||
**Source**: [`SimpleXChat/ChatTypes.swift`](../SimpleXChat/ChatTypes.swift#L1435)
|
||||
|
||||
Represents the type and metadata of a conversation:
|
||||
|
||||
```swift
|
||||
public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
case direct(contact: Contact)
|
||||
case group(groupInfo: GroupInfo, groupChatScope: GroupChatScopeInfo?)
|
||||
case group(groupInfo: GroupInfo, groupChatScope: GroupChatScopeInfo?, channelMsgInfo: ChannelMsgInfo?)
|
||||
case local(noteFolder: NoteFolder)
|
||||
case contactRequest(contactRequest: UserContactRequest)
|
||||
case contactConnection(contactConnection: PendingContactConnection)
|
||||
@@ -356,12 +372,23 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
| Case | Associated Value | Description |
|
||||
|------|-----------------|-------------|
|
||||
| `.direct` | `Contact` | One-to-one conversation |
|
||||
| `.group` | `GroupInfo, GroupChatScopeInfo?` | Group conversation (optional scope for member support threads) |
|
||||
| `.group` | `GroupInfo, GroupChatScopeInfo?, ChannelMsgInfo?` | Group conversation. Optional `GroupChatScopeInfo` selects a member support thread; optional `ChannelMsgInfo` (set client-side only) selects a channel-comments thread under a specific post. The two are mutually exclusive in practice. |
|
||||
| `.local` | `NoteFolder` | Local notes (self-chat) |
|
||||
| `.contactRequest` | `UserContactRequest` | Incoming contact request |
|
||||
| `.contactConnection` | `PendingContactConnection` | Pending connection |
|
||||
| `.invalidJSON` | `Data?` | Undecodable chat data |
|
||||
|
||||
### [ChannelMsgInfo](../SimpleXChat/ChatTypes.swift#L2028-L2036)
|
||||
|
||||
```swift
|
||||
public struct ChannelMsgInfo: Decodable, Hashable {
|
||||
public var channelMsgItem: ChatItem
|
||||
public var channelMsgSharedId: String
|
||||
}
|
||||
```
|
||||
|
||||
Local-only carrier for the comments-thread view. Holds the parent channel post (`channelMsgItem`) and its shared message id (`channelMsgSharedId`, hoisted from `parent.meta.itemSharedMsgId`). The struct is `Decodable` so it can sit inside the `ChatInfo.group` enum case, but the Haskell core never emits this field — `ItemsModel.loadSecondaryChat` injects it after fetching the thread by `parentItemId=`. See [§3 Channel comments thread](#channel-comments-thread).
|
||||
|
||||
### Key Computed Properties on ChatInfo
|
||||
|
||||
| Property | Type | Description |
|
||||
@@ -382,25 +409,46 @@ A **channel** is a group with `groupInfo.useRelays == true`. These types support
|
||||
| Type | Field | Type | Description | Line |
|
||||
|------|-------|------|-------------|------|
|
||||
| `User` | `userChatRelay` | `Bool` | Whether user acts as a chat relay | [L46](../SimpleXChat/ChatTypes.swift#L46) |
|
||||
| `GroupInfo` | `useRelays` | `Bool` | Whether group uses relay infrastructure (channel mode) | [L2343](../SimpleXChat/ChatTypes.swift#L2343) |
|
||||
| `GroupInfo` | `relayOwnStatus` | `RelayStatus?` | Current user's relay status in this group | [L2344](../SimpleXChat/ChatTypes.swift#L2344) |
|
||||
| `GroupProfile` | `publicGroup` | `PublicGroupProfile?` | Channel-specific profile data (type, link, ID) | [L2472](../SimpleXChat/ChatTypes.swift#L2472) |
|
||||
| `GroupInfo` | `useRelays` | `Bool` | Whether group uses relay infrastructure (channel mode) | [L2448](../SimpleXChat/ChatTypes.swift#L2448) |
|
||||
| `GroupInfo` | `relayOwnStatus` | `RelayStatus?` | Current user's relay status in this group | [L2449](../SimpleXChat/ChatTypes.swift#L2449) |
|
||||
| `GroupProfile` | `publicGroup` | `PublicGroupProfile?` | Channel-specific profile data (type, link, ID) | [L2588](../SimpleXChat/ChatTypes.swift#L2588) |
|
||||
|
||||
#### New Types
|
||||
|
||||
| Type | Kind | Description | Line |
|
||||
|------|------|-------------|------|
|
||||
| `RelayStatus` | `enum` | Relay lifecycle: `.rsNew`, `.rsInvited`, `.rsAccepted`, `.rsActive` | [L2506](../SimpleXChat/ChatTypes.swift#L2506) |
|
||||
| `RelayStatus.text` | `extension` | Localized display text: New/Invited/Accepted/Active | [L2565](../SimpleXChat/ChatTypes.swift#L2565) |
|
||||
| `GroupRelay` | `struct` | Relay instance for a group (ID, member ID, relay status). Fetched at runtime via `apiGetGroupRelays` (owner only) | [L2555](../SimpleXChat/ChatTypes.swift#L2555) |
|
||||
| `UserChatRelay` | `struct` | User's chat relay configuration (ID, SMP address, name, domains, preset/tested/enabled/deleted flags) | [L2513](../SimpleXChat/ChatTypes.swift#L2513) |
|
||||
| `RelayStatus` | `enum` | Relay lifecycle: `.rsNew`, `.rsInvited`, `.rsAccepted`, `.rsActive` | [L2659](../SimpleXChat/ChatTypes.swift#L2659) |
|
||||
| `RelayStatus.text` | `extension` | Localized display text: New/Invited/Accepted/Active | [L2730](../SimpleXChat/ChatTypes.swift#L2730) |
|
||||
| `GroupRelay` | `struct` | Relay instance for a group (ID, member ID, relay status). Fetched at runtime via `apiGetGroupRelays` (owner only) | [L2721](../SimpleXChat/ChatTypes.swift#L2721) |
|
||||
| `UserChatRelay` | `struct` | User's chat relay configuration (ID, SMP address, name, domains, preset/tested/enabled/deleted flags) | [L2674](../SimpleXChat/ChatTypes.swift#L2674) |
|
||||
|
||||
#### New Enum Cases
|
||||
|
||||
| Enum | Case | Description | Line |
|
||||
|------|------|-------------|------|
|
||||
| `GroupMemberRole` | `.relay` | Role for relay members (below `.observer`) | [L2807](../SimpleXChat/ChatTypes.swift#L2807) |
|
||||
| `CIDirection` | `.channelRcv` | Message direction for channel-received messages (via relay) | [L3529](../SimpleXChat/ChatTypes.swift#L3529) |
|
||||
| `GroupMemberRole` | `.relay` | Role for relay members (below `.observer`) | [L2974](../SimpleXChat/ChatTypes.swift#L2974) |
|
||||
| `CIDirection` | `.channelRcv` | Message direction for channel-received messages (via relay) | [L3737](../SimpleXChat/ChatTypes.swift#L3737) |
|
||||
|
||||
### Comments-Related Data Model (Channel Comments)
|
||||
|
||||
A **comment** is a `ChatItem` whose `parentChatItemId` references a channel post. The parent post is the only `ChatItem` that carries `commentsTotal`/`commentsDisabled`. Comments live in a secondary `ItemsModel` keyed by [`.groupChannelMsgContext(parent:)`](#secondaryitemsmodelfilter); the channel-post-list view (slice 4) uses `commentsTotal` to render the comments-button counter.
|
||||
|
||||
#### New Fields on Existing Types
|
||||
|
||||
| Type | Field | Type | Description | Line |
|
||||
|------|-------|------|-------------|------|
|
||||
| `ChatItem` | `parentChatItemId` | `Int64?` | Parent post id if this item is a comment. Hoisted from `CIMeta.parentChatItemId` on decode for caller ergonomics; `nil` means the item is not a comment. | [L3289](../SimpleXChat/ChatTypes.swift#L3289) |
|
||||
| `ChatItem` | `commentsTotal` | `Int` | Total comments under this item (only meaningful when this item is a channel post). Defaults to `0` via `decodeIfPresent ?? 0`, mirroring Haskell `omittedField`. | [L3290](../SimpleXChat/ChatTypes.swift#L3290) |
|
||||
| `ChatItem` | `commentsDisabled` | `Bool` | Whether the channel owner has disabled comments under this post. Defaults to `false` via `decodeIfPresent ?? false`. | [L3291](../SimpleXChat/ChatTypes.swift#L3291) |
|
||||
| `CIMeta` | `itemSharedMsgId` | `String?` | Shared message id (`SharedMsgId`) used to address this item across members. Needed to open a comments thread under an owner-side parent that has just been sent. | [L3783](../SimpleXChat/ChatTypes.swift#L3783) |
|
||||
|
||||
The three `ChatItem` fields are wire-encoded inside the nested `meta` object (Haskell `CIMeta`). The iOS [`ChatItem.init(from decoder:)`](../SimpleXChat/ChatTypes.swift#L3304-L3320) hoists them to `ChatItem` level so that view code can write `ci.parentChatItemId` and `ci.commentsTotal` directly without descending into `ci.meta`. Encoding round-trips remain wire-compatible because the iOS `ChatItem` is `Decodable`-only.
|
||||
|
||||
#### New Types
|
||||
|
||||
| Type | Kind | Description | Line |
|
||||
|------|------|-------------|------|
|
||||
| `ChannelMsgInfo` | `struct` | Local-only carrier for the comments-thread view. See [§7 ChannelMsgInfo](#channelmsginfo). | [L2028](../SimpleXChat/ChatTypes.swift#L2028) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user