mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-02 15:41:44 +00:00
ios: knocking ui (#5851)
* ios: knocking ui types * update types * member admission * remove chatItemStatuses * member support view * member support chat view wip * ios: secondary ItemsModel (#5862) * toolbar * more chats * remove theme * preview icon * chat toolbar fast markers * change icon * change icon * remove/accept buttons * item style * get item ItemsModel in chat model methods (chat view doesn't work dynamically) * fix support chat * fix other chats * refresh on exit * refresh button * dynamic marker in chat list * prohibit multi select actions * prohibited send field
This commit is contained in:
@@ -39,9 +39,9 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case apiGetSettings(settings: AppSettings)
|
||||
case apiGetChatTags(userId: Int64)
|
||||
case apiGetChats(userId: Int64)
|
||||
case apiGetChat(chatId: ChatId, pagination: ChatPagination, search: String)
|
||||
case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64)
|
||||
case apiSendMessages(type: ChatType, id: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
|
||||
case apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag?, pagination: ChatPagination, search: String)
|
||||
case apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64)
|
||||
case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
|
||||
case apiCreateChatTag(tag: ChatTagData)
|
||||
case apiSetChatTags(type: ChatType, id: Int64, tagIds: [Int64])
|
||||
case apiDeleteChatTag(tagId: Int64)
|
||||
@@ -49,15 +49,15 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case apiReorderChatTags(tagIds: [Int64])
|
||||
case apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage])
|
||||
case apiReportMessage(groupId: Int64, chatItemId: Int64, reportReason: ReportReason, reportText: String)
|
||||
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool)
|
||||
case apiDeleteChatItem(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode)
|
||||
case apiUpdateChatItem(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool)
|
||||
case apiDeleteChatItem(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64], mode: CIDeleteMode)
|
||||
case apiDeleteMemberChatItem(groupId: Int64, itemIds: [Int64])
|
||||
case apiArchiveReceivedReports(groupId: Int64)
|
||||
case apiDeleteReceivedReports(groupId: Int64, itemIds: [Int64], mode: CIDeleteMode)
|
||||
case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction)
|
||||
case apiChatItemReaction(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64, add: Bool, reaction: MsgReaction)
|
||||
case apiGetReactionMembers(userId: Int64, groupId: Int64, itemId: Int64, reaction: MsgReaction)
|
||||
case apiPlanForwardChatItems(toChatType: ChatType, toChatId: Int64, itemIds: [Int64])
|
||||
case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?)
|
||||
case apiPlanForwardChatItems(fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64])
|
||||
case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64], ttl: Int?)
|
||||
case apiGetNtfToken
|
||||
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
|
||||
case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
|
||||
@@ -68,6 +68,7 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case apiNewGroup(userId: Int64, incognito: Bool, groupProfile: GroupProfile)
|
||||
case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole)
|
||||
case apiJoinGroup(groupId: Int64)
|
||||
case apiAcceptMember(groupId: Int64, groupMemberId: Int64, memberRole: GroupMemberRole)
|
||||
case apiMembersRole(groupId: Int64, memberIds: [Int64], memberRole: GroupMemberRole)
|
||||
case apiBlockMembersForAll(groupId: Int64, memberIds: [Int64], blocked: Bool)
|
||||
case apiRemoveMembers(groupId: Int64, memberIds: [Int64], withMessages: Bool)
|
||||
@@ -147,8 +148,8 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
|
||||
// WebRTC calls /
|
||||
case apiGetNetworkStatuses
|
||||
case apiChatRead(type: ChatType, id: Int64)
|
||||
case apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64])
|
||||
case apiChatRead(type: ChatType, id: Int64, scope: GroupChatScope?)
|
||||
case apiChatItemsRead(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64])
|
||||
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
|
||||
case receiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?, inline: Bool?)
|
||||
case setFileToReceive(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?)
|
||||
@@ -209,15 +210,16 @@ 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, pagination, search): return "/_get chat \(chatId) \(pagination.cmdString)" +
|
||||
(search == "" ? "" : " search=\(search)")
|
||||
case let .apiGetChatItemInfo(type, id, itemId): return "/_get item info \(ref(type, id)) \(itemId)"
|
||||
case let .apiSendMessages(type, id, live, ttl, composedMessages):
|
||||
case let .apiGetChat(chatId, scope, contentTag, pagination, search):
|
||||
let tag = contentTag != nil ? " content=\(contentTag!.rawValue)" : ""
|
||||
return "/_get chat \(chatId)\(scopeRef(scope: scope))\(tag) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)")
|
||||
case let .apiGetChatItemInfo(type, id, scope, itemId): return "/_get item info \(ref(type, id, scope: scope)) \(itemId)"
|
||||
case let .apiSendMessages(type, id, scope, live, ttl, composedMessages):
|
||||
let msgs = encodeJSON(composedMessages)
|
||||
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
|
||||
return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
|
||||
return "/_send \(ref(type, id, scope: scope)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
|
||||
case let .apiCreateChatTag(tag): return "/_create tag \(encodeJSON(tag))"
|
||||
case let .apiSetChatTags(type, id, tagIds): return "/_tags \(ref(type, id)) \(tagIds.map({ "\($0)" }).joined(separator: ","))"
|
||||
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)"
|
||||
case let .apiUpdateChatTag(tagId, tagData): return "/_update tag \(tagId) \(encodeJSON(tagData))"
|
||||
case let .apiReorderChatTags(tagIds): return "/_reorder tags \(tagIds.map({ "\($0)" }).joined(separator: ","))"
|
||||
@@ -226,17 +228,17 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
return "/_create *\(noteFolderId) json \(msgs)"
|
||||
case let .apiReportMessage(groupId, chatItemId, reportReason, reportText):
|
||||
return "/_report #\(groupId) \(chatItemId) reason=\(reportReason) \(reportText)"
|
||||
case let .apiUpdateChatItem(type, id, itemId, um, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(um.cmdString)"
|
||||
case let .apiDeleteChatItem(type, id, itemIds, mode): return "/_delete item \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)"
|
||||
case let .apiUpdateChatItem(type, id, scope, itemId, um, live): return "/_update item \(ref(type, id, scope: scope)) \(itemId) live=\(onOff(live)) \(um.cmdString)"
|
||||
case let .apiDeleteChatItem(type, id, scope, itemIds, mode): return "/_delete item \(ref(type, id, scope: scope)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)"
|
||||
case let .apiDeleteMemberChatItem(groupId, itemIds): return "/_delete member item #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ","))"
|
||||
case let .apiArchiveReceivedReports(groupId): return "/_archive reports #\(groupId)"
|
||||
case let .apiDeleteReceivedReports(groupId, itemIds, mode): return "/_delete reports #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)"
|
||||
case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))"
|
||||
case let .apiChatItemReaction(type, id, scope, itemId, add, reaction): return "/_reaction \(ref(type, id, scope: scope)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))"
|
||||
case let .apiGetReactionMembers(userId, groupId, itemId, reaction): return "/_reaction members \(userId) #\(groupId) \(itemId) \(encodeJSON(reaction))"
|
||||
case let .apiPlanForwardChatItems(type, id, itemIds): return "/_forward plan \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ","))"
|
||||
case let .apiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl):
|
||||
case let .apiPlanForwardChatItems(type, id, scope, itemIds): return "/_forward plan \(ref(type, id, scope: scope)) \(itemIds.map({ "\($0)" }).joined(separator: ","))"
|
||||
case let .apiForwardChatItems(toChatType, toChatId, toScope, fromChatType, fromChatId, fromScope, itemIds, ttl):
|
||||
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
|
||||
return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) ttl=\(ttlStr)"
|
||||
return "/_forward \(ref(toChatType, toChatId, scope: toScope)) \(ref(fromChatType, fromChatId, scope: fromScope)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) ttl=\(ttlStr)"
|
||||
case .apiGetNtfToken: return "/_ntf get "
|
||||
case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)"
|
||||
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
|
||||
@@ -247,6 +249,7 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case let .apiNewGroup(userId, incognito, groupProfile): return "/_group \(userId) incognito=\(onOff(incognito)) \(encodeJSON(groupProfile))"
|
||||
case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)"
|
||||
case let .apiJoinGroup(groupId): return "/_join #\(groupId)"
|
||||
case let .apiAcceptMember(groupId, groupMemberId, memberRole): return "/_accept member #\(groupId) \(groupMemberId) \(memberRole.rawValue)"
|
||||
case let .apiMembersRole(groupId, memberIds, memberRole): return "/_member role #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) \(memberRole.rawValue)"
|
||||
case let .apiBlockMembersForAll(groupId, memberIds, blocked): return "/_block #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) blocked=\(onOff(blocked))"
|
||||
case let .apiRemoveMembers(groupId, memberIds, withMessages): return "/_remove #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) messages=\(onOff(withMessages))"
|
||||
@@ -270,13 +273,13 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case let .apiAcceptConditions(conditionsId, operatorIds): return "/_accept_conditions \(conditionsId) \(joinedIds(operatorIds))"
|
||||
case let .apiSetChatItemTTL(userId, seconds): return "/_ttl \(userId) \(chatItemTTLStr(seconds: seconds))"
|
||||
case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)"
|
||||
case let .apiSetChatTTL(userId, type, id, seconds): return "/_ttl \(userId) \(ref(type, id)) \(chatItemTTLStr(seconds: seconds))"
|
||||
case let .apiSetChatTTL(userId, type, id, seconds): return "/_ttl \(userId) \(ref(type, id, scope: nil)) \(chatItemTTLStr(seconds: seconds))"
|
||||
case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))"
|
||||
case .apiGetNetworkConfig: return "/network"
|
||||
case let .apiSetNetworkInfo(networkInfo): return "/_network info \(encodeJSON(networkInfo))"
|
||||
case .reconnectAllServers: return "/reconnect"
|
||||
case let .reconnectServer(userId, smpServer): return "/reconnect \(userId) \(smpServer)"
|
||||
case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))"
|
||||
case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id, scope: nil)) \(encodeJSON(chatSettings))"
|
||||
case let .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))"
|
||||
case let .apiContactInfo(contactId): return "/_info @\(contactId)"
|
||||
case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)"
|
||||
@@ -308,8 +311,8 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case let .apiConnectPlan(userId, connLink): return "/_connect plan \(userId) \(connLink)"
|
||||
case let .apiConnect(userId, incognito, connLink): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connLink.connFullLink) \(connLink.connShortLink ?? "")"
|
||||
case let .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)"
|
||||
case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id)) \(chatDeleteMode.cmdString)"
|
||||
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
|
||||
case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id, scope: nil)) \(chatDeleteMode.cmdString)"
|
||||
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id, scope: nil))"
|
||||
case let .apiListContacts(userId): return "/_contacts \(userId)"
|
||||
case let .apiUpdateProfile(userId, profile): return "/_profile \(userId) \(encodeJSON(profile))"
|
||||
case let .apiSetContactPrefs(contactId, preferences): return "/_set prefs @\(contactId) \(encodeJSON(preferences))"
|
||||
@@ -334,9 +337,9 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case .apiGetCallInvitations: return "/_call get"
|
||||
case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)"
|
||||
case .apiGetNetworkStatuses: return "/_network_statuses"
|
||||
case let .apiChatRead(type, id): return "/_read chat \(ref(type, id))"
|
||||
case let .apiChatItemsRead(type, id, itemIds): return "/_read chat items \(ref(type, id)) \(joinedIds(itemIds))"
|
||||
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
|
||||
case let .apiChatRead(type, id, scope): return "/_read chat \(ref(type, id, scope: scope))"
|
||||
case let .apiChatItemsRead(type, id, scope, itemIds): return "/_read chat items \(ref(type, id, scope: scope)) \(joinedIds(itemIds))"
|
||||
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id, scope: nil)) \(onOff(unreadChat))"
|
||||
case let .receiveFile(fileId, userApprovedRelays, encrypt, inline): return "/freceive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))"
|
||||
case let .setFileToReceive(fileId, userApprovedRelays, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))"
|
||||
case let .cancelFile(fileId): return "/fcancel \(fileId)"
|
||||
@@ -421,6 +424,7 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
case .apiNewGroup: return "apiNewGroup"
|
||||
case .apiAddMember: return "apiAddMember"
|
||||
case .apiJoinGroup: return "apiJoinGroup"
|
||||
case .apiAcceptMember: return "apiAcceptMember"
|
||||
case .apiMembersRole: return "apiMembersRole"
|
||||
case .apiBlockMembersForAll: return "apiBlockMembersForAll"
|
||||
case .apiRemoveMembers: return "apiRemoveMembers"
|
||||
@@ -523,8 +527,20 @@ enum ChatCommand: ChatCmdProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func ref(_ type: ChatType, _ id: Int64) -> String {
|
||||
"\(type.rawValue)\(id)"
|
||||
func ref(_ type: ChatType, _ id: Int64, scope: GroupChatScope?) -> String {
|
||||
"\(type.rawValue)\(id)\(scopeRef(scope: scope))"
|
||||
}
|
||||
|
||||
func scopeRef(scope: GroupChatScope?) -> String {
|
||||
switch (scope) {
|
||||
case .none: ""
|
||||
case let .memberSupport(groupMemberId_):
|
||||
if let groupMemberId = groupMemberId_ {
|
||||
"(_support:\(groupMemberId))"
|
||||
} else {
|
||||
"(_support)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func joinedIds(_ ids: [Int64]) -> String {
|
||||
@@ -676,6 +692,8 @@ enum ChatResponse: Decodable, Error, ChatRespProtocol {
|
||||
case receivedGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, memberRole: GroupMemberRole)
|
||||
case groupDeletedUser(user: UserRef, groupInfo: GroupInfo)
|
||||
case joinedGroupMemberConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, member: GroupMember)
|
||||
case memberAccepted(user: UserRef, groupInfo: GroupInfo, member: GroupMember)
|
||||
case memberAcceptedByOther(user: UserRef, groupInfo: GroupInfo, acceptingMember: GroupMember, member: GroupMember)
|
||||
case memberRole(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole)
|
||||
case membersRoleUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], toRole: GroupMemberRole)
|
||||
case memberBlockedForAll(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, blocked: Bool)
|
||||
@@ -856,6 +874,8 @@ enum ChatResponse: Decodable, Error, ChatRespProtocol {
|
||||
case .receivedGroupInvitation: return "receivedGroupInvitation"
|
||||
case .groupDeletedUser: return "groupDeletedUser"
|
||||
case .joinedGroupMemberConnecting: return "joinedGroupMemberConnecting"
|
||||
case .memberAccepted: return "memberAccepted"
|
||||
case .memberAcceptedByOther: return "memberAcceptedByOther"
|
||||
case .memberRole: return "memberRole"
|
||||
case .membersRoleUser: return "membersRoleUser"
|
||||
case .memberBlockedForAll: return "memberBlockedForAll"
|
||||
@@ -1043,6 +1063,8 @@ enum ChatResponse: Decodable, Error, ChatRespProtocol {
|
||||
case let .receivedGroupInvitation(u, groupInfo, contact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmemberRole: \(memberRole)")
|
||||
case let .groupDeletedUser(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .joinedGroupMemberConnecting(u, groupInfo, hostMember, member): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(hostMember)\nmember: \(member)")
|
||||
case let .memberAccepted(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .memberAcceptedByOther(u, groupInfo, acceptingMember, member): return withUser(u, "groupInfo: \(groupInfo)\nacceptingMember: \(acceptingMember)\nmember: \(member)")
|
||||
case let .memberRole(u, groupInfo, byMember, member, fromRole, toRole): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nfromRole: \(fromRole)\ntoRole: \(toRole)")
|
||||
case let .membersRoleUser(u, groupInfo, members, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\ntoRole: \(toRole)")
|
||||
case let .memberBlockedForAll(u, groupInfo, byMember, member, blocked): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nblocked: \(blocked)")
|
||||
|
||||
@@ -43,8 +43,26 @@ private func addTermItem(_ items: inout [TerminalItem], _ item: TerminalItem) {
|
||||
items.append(item)
|
||||
}
|
||||
|
||||
// analogue for SecondaryContextFilter in Kotlin
|
||||
enum SecondaryItemsModelFilter {
|
||||
case groupChatScopeContext(groupScopeInfo: GroupChatScopeInfo)
|
||||
case msgContentTagContext(contentTag: MsgContentTag)
|
||||
|
||||
func descr() -> String {
|
||||
switch self {
|
||||
case let .groupChatScopeContext(groupScopeInfo):
|
||||
return "groupChatScopeContext \(groupScopeInfo.toChatScope())"
|
||||
case let .msgContentTagContext(contentTag):
|
||||
return "msgContentTagContext \(contentTag.rawValue)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// analogue for ChatsContext in Kotlin
|
||||
class ItemsModel: ObservableObject {
|
||||
static let shared = ItemsModel()
|
||||
static let shared = ItemsModel(secondaryIMFilter: nil)
|
||||
public var secondaryIMFilter: SecondaryItemsModelFilter?
|
||||
public var preloadState = PreloadState()
|
||||
private let publisher = ObservableObjectPublisher()
|
||||
private var bag = Set<AnyCancellable>()
|
||||
var reversedChatItems: [ChatItem] = [] {
|
||||
@@ -68,7 +86,8 @@ class ItemsModel: ObservableObject {
|
||||
chatState.splits.isEmpty || chatState.splits.first != reversedChatItems.first?.id
|
||||
}
|
||||
|
||||
init() {
|
||||
init(secondaryIMFilter: SecondaryItemsModelFilter? = nil) {
|
||||
self.secondaryIMFilter = secondaryIMFilter
|
||||
publisher
|
||||
.throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true)
|
||||
.sink { self.objectWillChange.send() }
|
||||
@@ -83,6 +102,9 @@ class ItemsModel: ObservableObject {
|
||||
try await Task.sleep(nanoseconds: 250_000000)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.chatId = chatId
|
||||
if secondaryIMFilter != nil {
|
||||
ChatModel.shared.secondaryIM = self
|
||||
}
|
||||
willNavigate()
|
||||
}
|
||||
} catch {}
|
||||
@@ -90,7 +112,7 @@ class ItemsModel: ObservableObject {
|
||||
loadChatTask = Task {
|
||||
await MainActor.run { self.isLoading = true }
|
||||
// try? await Task.sleep(nanoseconds: 1000_000000)
|
||||
await loadChat(chatId: chatId)
|
||||
await loadChat(chatId: chatId, im: self)
|
||||
if !Task.isCancelled {
|
||||
await MainActor.run {
|
||||
self.isLoading = false
|
||||
@@ -105,7 +127,7 @@ class ItemsModel: ObservableObject {
|
||||
loadChatTask?.cancel()
|
||||
loadChatTask = Task {
|
||||
// try? await Task.sleep(nanoseconds: 1000_000000)
|
||||
await loadChat(chatId: chatId, openAroundItemId: openAroundItemId, clearItems: openAroundItemId == nil)
|
||||
await loadChat(chatId: chatId, im: self, openAroundItemId: openAroundItemId, clearItems: openAroundItemId == nil)
|
||||
if !Task.isCancelled {
|
||||
await MainActor.run {
|
||||
if openAroundItemId == nil {
|
||||
@@ -115,6 +137,34 @@ class ItemsModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var contentTag: MsgContentTag? {
|
||||
switch secondaryIMFilter {
|
||||
case nil: nil
|
||||
case .groupChatScopeContext: nil
|
||||
case let .msgContentTagContext(contentTag): contentTag
|
||||
}
|
||||
}
|
||||
|
||||
public var groupScopeInfo: GroupChatScopeInfo? {
|
||||
switch secondaryIMFilter {
|
||||
case nil: nil
|
||||
case let .groupChatScopeContext(scopeInfo): scopeInfo
|
||||
case .msgContentTagContext: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PreloadState {
|
||||
var prevFirstVisible: Int64 = Int64.min
|
||||
var prevItemsCount: Int = 0
|
||||
var preloading: Bool = false
|
||||
|
||||
func clear() {
|
||||
prevFirstVisible = Int64.min
|
||||
prevItemsCount = 0
|
||||
preloading = false
|
||||
}
|
||||
}
|
||||
|
||||
class ChatTagsModel: ObservableObject {
|
||||
@@ -278,7 +328,6 @@ final class ChatModel: ObservableObject {
|
||||
// current chat
|
||||
@Published var chatId: String?
|
||||
@Published var openAroundItemId: ChatItem.ID? = nil
|
||||
var chatItemStatuses: Dictionary<Int64, CIStatus> = [:]
|
||||
@Published var chatToTop: String?
|
||||
@Published var groupMembers: [GMember] = []
|
||||
@Published var groupMembersIndexes: Dictionary<Int64, Int> = [:] // groupMemberId to index in groupMembers list
|
||||
@@ -327,6 +376,9 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
let im = ItemsModel.shared
|
||||
|
||||
// ItemsModel for secondary chat view (such as support scope chat), as opposed to ItemsModel.shared used for primary chat
|
||||
@Published var secondaryIM: ItemsModel? = nil
|
||||
|
||||
static var ok: Bool { ChatModel.shared.chatDbStatus == .ok }
|
||||
|
||||
let ntfEnableLocal = true
|
||||
@@ -384,7 +436,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
|
||||
@@ -459,7 +511,7 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
func updateGroup(_ groupInfo: GroupInfo) {
|
||||
updateChat(.group(groupInfo: groupInfo))
|
||||
updateChat(.group(groupInfo: groupInfo, groupChatScope: nil))
|
||||
}
|
||||
|
||||
private func updateChat(_ cInfo: ChatInfo, addMissing: Bool = true) {
|
||||
@@ -502,77 +554,105 @@ final class ChatModel: ObservableObject {
|
||||
// }
|
||||
|
||||
func addChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
// mark chat non deleted
|
||||
if case let .direct(contact) = cInfo, contact.chatDeleted {
|
||||
var updatedContact = contact
|
||||
updatedContact.chatDeleted = false
|
||||
updateContact(updatedContact)
|
||||
}
|
||||
// update previews
|
||||
if let i = getChatIndex(cInfo.id) {
|
||||
chats[i].chatItems = switch cInfo {
|
||||
case .group:
|
||||
if let currentPreviewItem = chats[i].chatItems.first {
|
||||
if cItem.meta.itemTs >= currentPreviewItem.meta.itemTs {
|
||||
[cItem]
|
||||
// update chat list
|
||||
if cInfo.groupChatScope() == nil {
|
||||
// mark chat non deleted
|
||||
if case let .direct(contact) = cInfo, contact.chatDeleted {
|
||||
var updatedContact = contact
|
||||
updatedContact.chatDeleted = false
|
||||
updateContact(updatedContact)
|
||||
}
|
||||
// update preview
|
||||
if let i = getChatIndex(cInfo.id) {
|
||||
chats[i].chatItems = switch cInfo {
|
||||
case .group:
|
||||
if let currentPreviewItem = chats[i].chatItems.first {
|
||||
if cItem.meta.itemTs >= currentPreviewItem.meta.itemTs {
|
||||
[cItem]
|
||||
} else {
|
||||
[currentPreviewItem]
|
||||
}
|
||||
} else {
|
||||
[currentPreviewItem]
|
||||
[cItem]
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
[cItem]
|
||||
}
|
||||
default:
|
||||
[cItem]
|
||||
if case .rcvNew = cItem.meta.itemStatus {
|
||||
unreadCollector.changeUnreadCounter(cInfo.id, by: 1, unreadMentions: cItem.meta.userMention ? 1 : 0)
|
||||
}
|
||||
popChatCollector.throttlePopChat(cInfo.id, currentPosition: i)
|
||||
} else {
|
||||
addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
|
||||
}
|
||||
if case .rcvNew = cItem.meta.itemStatus {
|
||||
unreadCollector.changeUnreadCounter(cInfo.id, by: 1, unreadMentions: cItem.meta.userMention ? 1 : 0)
|
||||
}
|
||||
popChatCollector.throttlePopChat(cInfo.id, currentPosition: i)
|
||||
} else {
|
||||
addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
|
||||
}
|
||||
// add to current chat
|
||||
if chatId == cInfo.id {
|
||||
_ = _upsertChatItem(cInfo, cItem)
|
||||
// add to current scope
|
||||
if let ciIM = getCIItemsModel(cInfo, cItem) {
|
||||
_ = _upsertChatItem(ciIM, cInfo, cItem)
|
||||
}
|
||||
}
|
||||
|
||||
func getCIItemsModel(_ cInfo: ChatInfo, _ ci: ChatItem) -> ItemsModel? {
|
||||
let cInfoScope = cInfo.groupChatScope()
|
||||
if let cInfoScope = cInfoScope {
|
||||
switch cInfoScope {
|
||||
case .memberSupport:
|
||||
switch secondaryIM?.secondaryIMFilter {
|
||||
case .none:
|
||||
return nil
|
||||
case let .groupChatScopeContext(groupScopeInfo):
|
||||
return (cInfo.id == chatId && sameChatScope(cInfoScope, groupScopeInfo.toChatScope())) ? secondaryIM : nil
|
||||
case let .msgContentTagContext(contentTag):
|
||||
return (cInfo.id == chatId && ci.isReport && contentTag == .report) ? secondaryIM : nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return cInfo.id == chatId ? im : nil
|
||||
}
|
||||
}
|
||||
|
||||
func upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
||||
// update previews
|
||||
var res: Bool
|
||||
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) {
|
||||
// update chat list
|
||||
var itemAdded: Bool = false
|
||||
if cInfo.groupChatScope() == 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) {
|
||||
chat.chatItems = [cItem]
|
||||
}
|
||||
} else {
|
||||
chat.chatItems = [cItem]
|
||||
}
|
||||
} else {
|
||||
chat.chatItems = [cItem]
|
||||
addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
|
||||
itemAdded = true
|
||||
}
|
||||
if cItem.isDeletedContent || cItem.meta.itemDeleted != nil {
|
||||
VoiceItemState.stopVoiceInChatView(cInfo, cItem)
|
||||
}
|
||||
res = false
|
||||
} else {
|
||||
addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
|
||||
res = true
|
||||
}
|
||||
if cItem.isDeletedContent || cItem.meta.itemDeleted != nil {
|
||||
VoiceItemState.stopVoiceInChatView(cInfo, cItem)
|
||||
// update current scope
|
||||
if let ciIM = getCIItemsModel(cInfo, cItem) {
|
||||
itemAdded = _upsertChatItem(ciIM, cInfo, cItem)
|
||||
}
|
||||
// update current chat
|
||||
return chatId == cInfo.id ? _upsertChatItem(cInfo, cItem) : res
|
||||
return itemAdded
|
||||
}
|
||||
|
||||
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
||||
if let i = getChatItemIndex(cItem) {
|
||||
_updateChatItem(at: i, with: cItem)
|
||||
ChatItemDummyModel.shared.sendUpdate()
|
||||
private func _upsertChatItem(_ ciIM: ItemsModel, _ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
||||
if let i = getChatItemIndex(ciIM, cItem) {
|
||||
let oldStatus = ciIM.reversedChatItems[i].meta.itemStatus
|
||||
let newStatus = cItem.meta.itemStatus
|
||||
var ci = cItem
|
||||
if shouldKeepOldSndCIStatus(oldStatus: oldStatus, newStatus: newStatus) {
|
||||
ci.meta.itemStatus = oldStatus
|
||||
}
|
||||
_updateChatItem(ciIM: ciIM, at: i, with: ci)
|
||||
ChatItemDummyModel.shared.sendUpdate() // TODO [knocking] review what's this
|
||||
return false
|
||||
} else {
|
||||
var ci = cItem
|
||||
if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
|
||||
ci.meta.itemStatus = status
|
||||
}
|
||||
im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
|
||||
im.chatState.itemAdded((ci.id, ci.isRcvNew), hasLiveDummy ? 1 : 0)
|
||||
im.itemAdded = true
|
||||
ciIM.reversedChatItems.insert(cItem, at: hasLiveDummy ? 1 : 0)
|
||||
ciIM.chatState.itemAdded((cItem.id, cItem.isRcvNew), hasLiveDummy ? 1 : 0)
|
||||
ciIM.itemAdded = true
|
||||
ChatItemDummyModel.shared.sendUpdate()
|
||||
return true
|
||||
}
|
||||
@@ -586,40 +666,42 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem, status: CIStatus? = nil) {
|
||||
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
|
||||
if let ciIM = getCIItemsModel(cInfo, cItem),
|
||||
let i = getChatItemIndex(ciIM, cItem) {
|
||||
withConditionalAnimation {
|
||||
_updateChatItem(at: i, with: cItem)
|
||||
_updateChatItem(ciIM: ciIM, at: i, with: cItem)
|
||||
}
|
||||
} else if let status = status {
|
||||
chatItemStatuses.updateValue(status, forKey: cItem.id)
|
||||
}
|
||||
}
|
||||
|
||||
private func _updateChatItem(at i: Int, with cItem: ChatItem) {
|
||||
im.reversedChatItems[i] = cItem
|
||||
im.reversedChatItems[i].viewTimestamp = .now
|
||||
private func _updateChatItem(ciIM: ItemsModel, at i: Int, with cItem: ChatItem) {
|
||||
ciIM.reversedChatItems[i] = cItem
|
||||
ciIM.reversedChatItems[i].viewTimestamp = .now
|
||||
}
|
||||
|
||||
func getChatItemIndex(_ cItem: ChatItem) -> Int? {
|
||||
im.reversedChatItems.firstIndex(where: { $0.id == cItem.id })
|
||||
func getChatItemIndex(_ ciIM: ItemsModel, _ cItem: ChatItem) -> Int? {
|
||||
ciIM.reversedChatItems.firstIndex(where: { $0.id == cItem.id })
|
||||
}
|
||||
|
||||
func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
if cItem.isRcvNew {
|
||||
unreadCollector.changeUnreadCounter(cInfo.id, by: -1, unreadMentions: cItem.meta.userMention ? -1 : 0)
|
||||
}
|
||||
// update previews
|
||||
if let chat = getChat(cInfo.id) {
|
||||
if let pItem = chat.chatItems.last, pItem.id == cItem.id {
|
||||
chat.chatItems = [ChatItem.deletedItemDummy()]
|
||||
// update chat list
|
||||
if cInfo.groupChatScope() == nil {
|
||||
if cItem.isRcvNew {
|
||||
unreadCollector.changeUnreadCounter(cInfo.id, by: -1, unreadMentions: cItem.meta.userMention ? -1 : 0)
|
||||
}
|
||||
// update previews
|
||||
if let chat = getChat(cInfo.id) {
|
||||
if let pItem = chat.chatItems.last, pItem.id == cItem.id {
|
||||
chat.chatItems = [ChatItem.deletedItemDummy()]
|
||||
}
|
||||
}
|
||||
}
|
||||
// remove from current chat
|
||||
if chatId == cInfo.id {
|
||||
if let i = getChatItemIndex(cItem) {
|
||||
// remove from current scope
|
||||
if let ciIM = getCIItemsModel(cInfo, cItem) {
|
||||
if let i = getChatItemIndex(ciIM, cItem) {
|
||||
withAnimation {
|
||||
let item = im.reversedChatItems.remove(at: i)
|
||||
im.chatState.itemsRemoved([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed())
|
||||
let item = ciIM.reversedChatItems.remove(at: i)
|
||||
ciIM.chatState.itemsRemoved([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -635,7 +717,7 @@ final class ChatModel: ObservableObject {
|
||||
if chatId == groupInfo.id {
|
||||
for i in 0..<im.reversedChatItems.count {
|
||||
if let updatedItem = removedUpdatedItem(im.reversedChatItems[i]) {
|
||||
_updateChatItem(at: i, with: updatedItem)
|
||||
_updateChatItem(ciIM: im, at: i, with: updatedItem) // TODO [knocking] review: use getCIItemsModel?
|
||||
}
|
||||
}
|
||||
} else if let chat = getChat(groupInfo.id),
|
||||
@@ -763,7 +845,6 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
// clear current chat
|
||||
if chatId == cInfo.id {
|
||||
chatItemStatuses = [:]
|
||||
im.reversedChatItems = []
|
||||
im.chatState.clear()
|
||||
}
|
||||
@@ -938,6 +1019,23 @@ final class ChatModel: ObservableObject {
|
||||
return unread
|
||||
}
|
||||
|
||||
func increaseGroupSupportChatsUnreadCounter(_ chatId: ChatId) {
|
||||
changeGroupSupportChatsUnreadCounter(chatId, 1)
|
||||
}
|
||||
|
||||
func decreaseGroupSupportChatsUnreadCounter(_ chatId: ChatId, by: Int = 1) {
|
||||
changeGroupSupportChatsUnreadCounter(chatId, -by)
|
||||
}
|
||||
|
||||
private func changeGroupSupportChatsUnreadCounter(_ chatId: ChatId, _ by: Int = 0) {
|
||||
if by == 0 { return }
|
||||
|
||||
if let i = getChatIndex(chatId) {
|
||||
let chat = chats[i]
|
||||
chat.chatStats.supportChatsUnreadCount = max(0, chat.chatStats.supportChatsUnreadCount + by)
|
||||
}
|
||||
}
|
||||
|
||||
func increaseGroupReportsCounter(_ chatId: ChatId) {
|
||||
changeGroupReportsCounter(chatId, 1)
|
||||
}
|
||||
@@ -964,7 +1062,7 @@ final class ChatModel: ObservableObject {
|
||||
var count = 0
|
||||
var ns: [String] = []
|
||||
if let ciCategory = chatItem.mergeCategory,
|
||||
var i = getChatItemIndex(chatItem) {
|
||||
var i = getChatItemIndex(im, chatItem) { // TODO [knocking] review: use getCIItemsModel?
|
||||
while i < im.reversedChatItems.count {
|
||||
let ci = im.reversedChatItems[i]
|
||||
if ci.mergeCategory != ciCategory { break }
|
||||
@@ -980,7 +1078,7 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
// returns the index of the passed item and the next item (it has smaller index)
|
||||
func getNextChatItem(_ ci: ChatItem) -> (Int?, ChatItem?) {
|
||||
if let i = getChatItemIndex(ci) {
|
||||
if let i = getChatItemIndex(im, ci) { // TODO [knocking] review: use getCIItemsModel?
|
||||
(i, i > 0 ? im.reversedChatItems[i - 1] : nil)
|
||||
} else {
|
||||
(nil, nil)
|
||||
@@ -1091,7 +1189,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)
|
||||
}
|
||||
}
|
||||
@@ -1146,9 +1244,9 @@ final class Chat: ObservableObject, Identifiable, ChatLike {
|
||||
var userCanSend: Bool {
|
||||
switch chatInfo {
|
||||
case .direct: return true
|
||||
case let .group(groupInfo):
|
||||
case let .group(groupInfo, groupChatScope):
|
||||
let m = groupInfo.membership
|
||||
return m.memberActive && m.memberRole >= .member
|
||||
return (m.memberActive && m.memberRole >= .member && !m.memberPending) || groupChatScope != nil
|
||||
case .local:
|
||||
return true
|
||||
default: return false
|
||||
@@ -1157,13 +1255,20 @@ final class Chat: ObservableObject, Identifiable, ChatLike {
|
||||
|
||||
var userIsObserver: Bool {
|
||||
switch chatInfo {
|
||||
case let .group(groupInfo):
|
||||
case let .group(groupInfo, _):
|
||||
let m = groupInfo.membership
|
||||
return m.memberActive && m.memberRole == .observer
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
var userIsPending: Bool {
|
||||
switch chatInfo {
|
||||
case let .group(groupInfo, _): groupInfo.membership.memberPending
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
var unreadTag: Bool {
|
||||
switch chatInfo.chatSettings?.enableNtfs {
|
||||
case .all: chatStats.unreadChat || chatStats.unreadCount > 0
|
||||
|
||||
@@ -328,43 +328,54 @@ func apiGetChatTagsAsync() async throws -> [ChatTag] {
|
||||
|
||||
let loadItemsPerPage = 50
|
||||
|
||||
func apiGetChat(chatId: ChatId, pagination: ChatPagination, search: String = "") async throws -> (Chat, NavigationInfo) {
|
||||
let r = await chatSendCmd(.apiGetChat(chatId: chatId, pagination: pagination, search: search))
|
||||
func apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag? = nil, pagination: ChatPagination, search: String = "") async throws -> (Chat, NavigationInfo) {
|
||||
let r = await chatSendCmd(.apiGetChat(chatId: chatId, scope: scope, contentTag: contentTag, pagination: pagination, search: search))
|
||||
if case let .apiChat(_, chat, navInfo) = r { return (Chat.init(chat), navInfo ?? NavigationInfo()) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func loadChat(chat: Chat, search: String = "", clearItems: Bool = true) async {
|
||||
await loadChat(chatId: chat.chatInfo.id, search: search, clearItems: clearItems)
|
||||
func loadChat(chat: Chat, im: ItemsModel, search: String = "", clearItems: Bool = true) async {
|
||||
await loadChat(chatId: chat.chatInfo.id, im: im, search: search, clearItems: clearItems)
|
||||
}
|
||||
|
||||
func loadChat(chatId: ChatId, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async {
|
||||
let m = ChatModel.shared
|
||||
let im = ItemsModel.shared
|
||||
func loadChat(chatId: ChatId, im: ItemsModel, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async {
|
||||
await MainActor.run {
|
||||
m.chatItemStatuses = [:]
|
||||
if clearItems {
|
||||
im.reversedChatItems = []
|
||||
ItemsModel.shared.chatState.clear()
|
||||
im.chatState.clear()
|
||||
}
|
||||
}
|
||||
await apiLoadMessages(chatId, openAroundItemId != nil ? .around(chatItemId: openAroundItemId!, count: loadItemsPerPage) : (search == "" ? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage)), im.chatState, search, openAroundItemId, { 0...0 })
|
||||
await apiLoadMessages(
|
||||
chatId,
|
||||
im,
|
||||
( // pagination
|
||||
openAroundItemId != nil
|
||||
? .around(chatItemId: openAroundItemId!, count: loadItemsPerPage)
|
||||
: (
|
||||
search == ""
|
||||
? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage)
|
||||
)
|
||||
),
|
||||
search,
|
||||
openAroundItemId,
|
||||
{ 0...0 }
|
||||
)
|
||||
}
|
||||
|
||||
func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -> ChatItemInfo {
|
||||
let r = await chatSendCmd(.apiGetChatItemInfo(type: type, id: id, itemId: itemId))
|
||||
func apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64) async throws -> ChatItemInfo {
|
||||
let r = await chatSendCmd(.apiGetChatItemInfo(type: type, id: id, scope: scope, itemId: itemId))
|
||||
if case let .chatItemInfo(_, _, chatItemInfo) = r { return chatItemInfo }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiPlanForwardChatItems(type: ChatType, id: Int64, itemIds: [Int64]) async throws -> ([Int64], ForwardConfirmation?) {
|
||||
let r = await chatSendCmd(.apiPlanForwardChatItems(toChatType: type, toChatId: id, itemIds: itemIds))
|
||||
func apiPlanForwardChatItems(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64]) async throws -> ([Int64], ForwardConfirmation?) {
|
||||
let r = await chatSendCmd(.apiPlanForwardChatItems(fromChatType: type, fromChatId: id, fromScope: scope, itemIds: itemIds))
|
||||
if case let .forwardPlan(_, chatItemIds, forwardConfimation) = r { return (chatItemIds, forwardConfimation) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? {
|
||||
let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemIds: itemIds, ttl: ttl)
|
||||
func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? {
|
||||
let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, toScope: toScope, fromChatType: fromChatType, fromChatId: fromChatId, fromScope: fromScope, itemIds: itemIds, ttl: ttl)
|
||||
return await processSendMessageCmd(toChatType: toChatType, cmd: cmd)
|
||||
}
|
||||
|
||||
@@ -396,8 +407,8 @@ func apiReorderChatTags(tagIds: [Int64]) async throws {
|
||||
try await sendCommandOkResp(.apiReorderChatTags(tagIds: tagIds))
|
||||
}
|
||||
|
||||
func apiSendMessages(type: ChatType, id: Int64, live: Bool = false, ttl: Int? = nil, composedMessages: [ComposedMessage]) async -> [ChatItem]? {
|
||||
let cmd: ChatCommand = .apiSendMessages(type: type, id: id, live: live, ttl: ttl, composedMessages: composedMessages)
|
||||
func apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, live: Bool = false, ttl: Int? = nil, composedMessages: [ComposedMessage]) async -> [ChatItem]? {
|
||||
let cmd: ChatCommand = .apiSendMessages(type: type, id: id, scope: scope, live: live, ttl: ttl, composedMessages: composedMessages)
|
||||
return await processSendMessageCmd(toChatType: type, cmd: cmd)
|
||||
}
|
||||
|
||||
@@ -474,14 +485,14 @@ private func createChatItemsErrorAlert(_ r: ChatResponse) {
|
||||
)
|
||||
}
|
||||
|
||||
func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool = false) async throws -> ChatItem {
|
||||
let r = await chatSendCmd(.apiUpdateChatItem(type: type, id: id, itemId: itemId, updatedMessage: updatedMessage, live: live), bgDelay: msgDelay)
|
||||
func apiUpdateChatItem(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool = false) async throws -> ChatItem {
|
||||
let r = await chatSendCmd(.apiUpdateChatItem(type: type, id: id, scope: scope, itemId: itemId, updatedMessage: updatedMessage, live: live), bgDelay: msgDelay)
|
||||
if case let .chatItemUpdated(_, aChatItem) = r { return aChatItem.chatItem }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction) async throws -> ChatItem {
|
||||
let r = await chatSendCmd(.apiChatItemReaction(type: type, id: id, itemId: itemId, add: add, reaction: reaction), bgDelay: msgDelay)
|
||||
func apiChatItemReaction(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64, add: Bool, reaction: MsgReaction) async throws -> ChatItem {
|
||||
let r = await chatSendCmd(.apiChatItemReaction(type: type, id: id, scope: scope, itemId: itemId, add: add, reaction: reaction), bgDelay: msgDelay)
|
||||
if case let .chatItemReaction(_, _, reaction) = r { return reaction.chatReaction.chatItem }
|
||||
throw r
|
||||
}
|
||||
@@ -493,8 +504,8 @@ func apiGetReactionMembers(groupId: Int64, itemId: Int64, reaction: MsgReaction)
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteChatItems(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] {
|
||||
let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemIds: itemIds, mode: mode), bgDelay: msgDelay)
|
||||
func apiDeleteChatItems(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] {
|
||||
let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, scope: scope, itemIds: itemIds, mode: mode), bgDelay: msgDelay)
|
||||
if case let .chatItemsDeleted(_, items, _) = r { return items }
|
||||
throw r
|
||||
}
|
||||
@@ -1212,12 +1223,12 @@ func apiRejectContactRequest(contactReqId: Int64) async throws {
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiChatRead(type: ChatType, id: Int64) async throws {
|
||||
try await sendCommandOkResp(.apiChatRead(type: type, id: id))
|
||||
func apiChatRead(type: ChatType, id: Int64, scope: GroupChatScope?) async throws {
|
||||
try await sendCommandOkResp(.apiChatRead(type: type, id: id, scope: scope))
|
||||
}
|
||||
|
||||
func apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) async throws {
|
||||
try await sendCommandOkResp(.apiChatItemsRead(type: type, id: id, itemIds: itemIds))
|
||||
func apiChatItemsRead(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64]) async throws {
|
||||
try await sendCommandOkResp(.apiChatItemsRead(type: type, id: id, scope: scope, itemIds: itemIds))
|
||||
}
|
||||
|
||||
func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
|
||||
@@ -1523,7 +1534,7 @@ func markChatRead(_ chat: Chat) async {
|
||||
do {
|
||||
if chat.chatStats.unreadCount > 0 {
|
||||
let cInfo = chat.chatInfo
|
||||
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId)
|
||||
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, scope: cInfo.groupChatScope())
|
||||
await MainActor.run {
|
||||
withAnimation { ChatModel.shared.markAllChatItemsRead(cInfo) }
|
||||
}
|
||||
@@ -1550,7 +1561,7 @@ func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async {
|
||||
|
||||
func apiMarkChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID], mentionsRead: Int) async {
|
||||
do {
|
||||
try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, itemIds: itemIds)
|
||||
try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, scope: cInfo.groupChatScope(), itemIds: itemIds)
|
||||
DispatchQueue.main.async {
|
||||
ChatModel.shared.markChatItemsRead(cInfo, itemIds, mentionsRead)
|
||||
}
|
||||
@@ -1600,6 +1611,12 @@ func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult {
|
||||
}
|
||||
}
|
||||
|
||||
func apiAcceptMember(_ groupId: Int64, _ groupMemberId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember {
|
||||
let r = await chatSendCmd(.apiAcceptMember(groupId: groupId, groupMemberId: groupMemberId, memberRole: memberRole))
|
||||
if case let .memberAccepted(_, _, member) = r { return member }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiRemoveMembers(_ groupId: Int64, _ memberIds: [Int64], _ withMessages: Bool = false) async throws -> [GroupMember] {
|
||||
let r = await chatSendCmd(.apiRemoveMembers(groupId: groupId, memberIds: memberIds, withMessages: withMessages), bgTask: false)
|
||||
if case let .userDeletedMembers(_, _, members, withMessages) = r { return members }
|
||||
@@ -2086,6 +2103,9 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
if cItem.isActiveReport {
|
||||
m.increaseGroupReportsCounter(cInfo.id)
|
||||
}
|
||||
if cInfo.groupChatScope() != nil && cItem.isRcvNew && cInfo.ntfsEnabled(chatItem: cItem) {
|
||||
m.increaseGroupSupportChatsUnreadCounter(cInfo.id)
|
||||
}
|
||||
} else if cItem.isRcvNew && cInfo.ntfsEnabled(chatItem: cItem) {
|
||||
m.increaseUnreadCounter(user: user)
|
||||
}
|
||||
@@ -2104,7 +2124,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
let cInfo = chatItem.chatInfo
|
||||
let cItem = chatItem.chatItem
|
||||
if !cItem.isDeletedContent && active(user) {
|
||||
await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) }
|
||||
_ = await MainActor.run { m.upsertChatItem(cInfo, cItem) }
|
||||
}
|
||||
if let endTask = m.messageDelivery[cItem.id] {
|
||||
switch cItem.meta.itemStatus {
|
||||
@@ -2200,6 +2220,12 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
}
|
||||
case let .memberAcceptedByOther(user, groupInfo, _, member):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
}
|
||||
case let .deletedMemberUser(user, groupInfo, member, withMessages): // TODO update user member
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
@@ -2231,6 +2257,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
}
|
||||
}
|
||||
case let .userJoinedGroup(user, groupInfo):
|
||||
// TODO [knocking] close support scope for this group if it's currently opened
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateGroup(groupInfo)
|
||||
@@ -2517,7 +2544,7 @@ func groupChatItemsDeleted(_ user: UserRef, _ groupInfo: GroupInfo, _ chatItemID
|
||||
return
|
||||
}
|
||||
let im = ItemsModel.shared
|
||||
let cInfo = ChatInfo.group(groupInfo: groupInfo)
|
||||
let cInfo = ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil)
|
||||
await MainActor.run {
|
||||
m.decreaseGroupReportsCounter(cInfo.id, by: chatItemIDs.count)
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ struct SimpleXApp: App {
|
||||
if let id = chatModel.chatId,
|
||||
let chat = chatModel.getChat(id),
|
||||
!NtfManager.shared.navigatingToChat {
|
||||
Task { await loadChat(chat: chat, clearItems: false) }
|
||||
Task { await loadChat(chat: chat, im: ItemsModel.shared, clearItems: false) }
|
||||
}
|
||||
if let ncr = chatModel.ntfContactRequest {
|
||||
await MainActor.run { chatModel.ntfContactRequest = nil }
|
||||
|
||||
@@ -22,11 +22,28 @@ struct ChatInfoToolbar: View {
|
||||
Image(systemName: "theatermasks").frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.indigo)
|
||||
Spacer().frame(width: 16)
|
||||
}
|
||||
ChatInfoImage(
|
||||
chat: chat,
|
||||
size: imageSize,
|
||||
color: Color(uiColor: .tertiaryLabel)
|
||||
)
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
ChatInfoImage(
|
||||
chat: chat,
|
||||
size: imageSize,
|
||||
color: Color(uiColor: .tertiaryLabel)
|
||||
)
|
||||
if chat.chatStats.reportsCount > 0 {
|
||||
Image(systemName: "flag.circle.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 14, height: 14)
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(.white, .red)
|
||||
} else if chat.chatStats.supportChatsUnreadCount > 0 {
|
||||
Image(systemName: "flag.circle.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 14, height: 14)
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(.white, theme.colors.primary)
|
||||
}
|
||||
}
|
||||
.padding(.trailing, 4)
|
||||
let t = Text(cInfo.displayName).font(.headline)
|
||||
(cInfo.contact?.verified == true ? contactVerifiedShield + t : t)
|
||||
|
||||
@@ -687,7 +687,7 @@ struct ChatTTLOption: View {
|
||||
let m = ChatModel.shared
|
||||
do {
|
||||
try await setChatTTL(chatType: chat.chatInfo.chatType, id: chat.chatInfo.apiId, ttl)
|
||||
await loadChat(chat: chat, clearItems: true)
|
||||
await loadChat(chat: chat, im: ItemsModel.shared, clearItems: true)
|
||||
await MainActor.run {
|
||||
progressIndicator = false
|
||||
currentChatItemTTL = chatItemTTL
|
||||
@@ -700,7 +700,7 @@ struct ChatTTLOption: View {
|
||||
}
|
||||
catch let error {
|
||||
logger.error("setChatTTL error \(responseError(error))")
|
||||
await loadChat(chat: chat, clearItems: true)
|
||||
await loadChat(chat: chat, im: ItemsModel.shared, clearItems: true)
|
||||
await MainActor.run {
|
||||
chatItemTTL = currentChatItemTTL
|
||||
progressIndicator = false
|
||||
@@ -938,7 +938,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()
|
||||
@@ -974,7 +974,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()
|
||||
@@ -1052,12 +1052,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))
|
||||
chat.wrappedValue = Chat.init(chatInfo: ChatInfo.group(groupInfo: groupInfo))
|
||||
ChatModel.shared.updateChatInfo(ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil))
|
||||
chat.wrappedValue = Chat.init(chatInfo: ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil))
|
||||
themes = themesFromChat(chat.wrappedValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import SimpleXChat
|
||||
struct CIChatFeatureView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@Environment(\.revealed) var revealed: Bool
|
||||
@ObservedObject var im = ItemsModel.shared
|
||||
@ObservedObject var chat: Chat
|
||||
@ObservedObject var im: ItemsModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var chatItem: ChatItem
|
||||
var feature: Feature
|
||||
@@ -53,7 +53,7 @@ struct CIChatFeatureView: View {
|
||||
private func mergedFeatures() -> [FeatureInfo]? {
|
||||
var fs: [FeatureInfo] = []
|
||||
var icons: Set<String> = []
|
||||
if var i = m.getChatItemIndex(chatItem) {
|
||||
if var i = m.getChatItemIndex(im, chatItem) {
|
||||
while i < im.reversedChatItems.count,
|
||||
let f = featureInfo(im.reversedChatItems[i]) {
|
||||
if !icons.contains(f.icon) {
|
||||
@@ -108,6 +108,7 @@ struct CIChatFeatureView_Previews: PreviewProvider {
|
||||
let enabled = FeatureEnabled(forUser: false, forContact: false)
|
||||
CIChatFeatureView(
|
||||
chat: Chat.sampleData,
|
||||
im: ItemsModel.shared,
|
||||
chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor(.secondary)
|
||||
).environment(\.revealed, true)
|
||||
}
|
||||
|
||||
@@ -278,6 +278,7 @@ func showFileErrorAlert(_ err: FileError, temporary: Bool = false) {
|
||||
|
||||
struct CIFileView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let im = ItemsModel.shared
|
||||
let sentFile: ChatItem = ChatItem(
|
||||
chatDir: .directSnd,
|
||||
meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true),
|
||||
@@ -293,16 +294,16 @@ struct CIFileView_Previews: PreviewProvider {
|
||||
file: nil
|
||||
)
|
||||
Group {
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: sentFile, scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: fileChatItemWtFile, scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: sentFile, scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: fileChatItemWtFile, scrollToItemId: { _ in })
|
||||
}
|
||||
.environment(\.revealed, false)
|
||||
.previewLayout(.fixed(width: 360, height: 360))
|
||||
|
||||
@@ -45,7 +45,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)
|
||||
@@ -83,7 +83,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 {
|
||||
|
||||
@@ -476,6 +476,7 @@ class VoiceItemState {
|
||||
|
||||
struct CIVoiceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let im = ItemsModel.shared
|
||||
let sentVoiceMessage: ChatItem = ChatItem(
|
||||
chatDir: .directSnd,
|
||||
meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true),
|
||||
@@ -498,10 +499,10 @@ struct CIVoiceView_Previews: PreviewProvider {
|
||||
duration: 30,
|
||||
allowMenu: Binding.constant(true)
|
||||
)
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, scrollToItemId: { _ in }, allowMenu: .constant(true))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), scrollToItemId: { _ in }, allowMenu: .constant(true))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItemId: { _ in }, allowMenu: .constant(true))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, scrollToItemId: { _ in }, allowMenu: .constant(true))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: sentVoiceMessage, scrollToItemId: { _ in }, allowMenu: .constant(true))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(), scrollToItemId: { _ in }, allowMenu: .constant(true))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItemId: { _ in }, allowMenu: .constant(true))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: voiceMessageWtFile, scrollToItemId: { _ in }, allowMenu: .constant(true))
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 360))
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ struct FramedCIVoiceView: View {
|
||||
|
||||
struct FramedCIVoiceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let im = ItemsModel.shared
|
||||
let sentVoiceMessage: ChatItem = ChatItem(
|
||||
chatDir: .directSnd,
|
||||
meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true),
|
||||
@@ -92,11 +93,11 @@ struct FramedCIVoiceView_Previews: PreviewProvider {
|
||||
file: CIFile.getSample(fileStatus: .sndComplete)
|
||||
)
|
||||
Group {
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWithQuote, scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: sentVoiceMessage, scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: voiceMessageWithQuote, scrollToItemId: { _ in })
|
||||
}
|
||||
.environment(\.revealed, false)
|
||||
.previewLayout(.fixed(width: 360, height: 360))
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -14,6 +14,7 @@ struct MarkedDeletedItemView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.revealed) var revealed: Bool
|
||||
@ObservedObject var chat: Chat
|
||||
@ObservedObject var im: ItemsModel
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
@@ -29,14 +30,14 @@ struct MarkedDeletedItemView: View {
|
||||
var mergedMarkedDeletedText: LocalizedStringKey {
|
||||
if !revealed,
|
||||
let ciCategory = chatItem.mergeCategory,
|
||||
var i = m.getChatItemIndex(chatItem) {
|
||||
var i = m.getChatItemIndex(im, chatItem) {
|
||||
var moderated = 0
|
||||
var blocked = 0
|
||||
var blockedByAdmin = 0
|
||||
var deleted = 0
|
||||
var moderatedBy: Set<String> = []
|
||||
while i < ItemsModel.shared.reversedChatItems.count,
|
||||
let ci = .some(ItemsModel.shared.reversedChatItems[i]),
|
||||
while i < im.reversedChatItems.count,
|
||||
let ci = .some(im.reversedChatItems[i]),
|
||||
ci.mergeCategory == ciCategory,
|
||||
let itemDeleted = ci.meta.itemDeleted {
|
||||
switch itemDeleted {
|
||||
@@ -85,6 +86,7 @@ struct MarkedDeletedItemView_Previews: PreviewProvider {
|
||||
Group {
|
||||
MarkedDeletedItemView(
|
||||
chat: Chat.sampleData,
|
||||
im: ItemsModel.shared,
|
||||
chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now))
|
||||
).environment(\.revealed, true)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ extension EnvironmentValues {
|
||||
|
||||
struct ChatItemView: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@ObservedObject var im: ItemsModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.showTimestamp) var showTimestamp: Bool
|
||||
@Environment(\.revealed) var revealed: Bool
|
||||
@@ -41,6 +42,7 @@ struct ChatItemView: View {
|
||||
|
||||
init(
|
||||
chat: Chat,
|
||||
im: ItemsModel,
|
||||
chatItem: ChatItem,
|
||||
scrollToItemId: @escaping (ChatItem.ID) -> Void,
|
||||
showMember: Bool = false,
|
||||
@@ -48,6 +50,7 @@ struct ChatItemView: View {
|
||||
allowMenu: Binding<Bool> = .constant(false)
|
||||
) {
|
||||
self.chat = chat
|
||||
self.im = im
|
||||
self.chatItem = chatItem
|
||||
self.scrollToItemId = scrollToItemId
|
||||
self.maxWidth = maxWidth
|
||||
@@ -57,14 +60,14 @@ struct ChatItemView: View {
|
||||
var body: some View {
|
||||
let ci = chatItem
|
||||
if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) {
|
||||
MarkedDeletedItemView(chat: chat, chatItem: chatItem)
|
||||
MarkedDeletedItemView(chat: chat, im: im, chatItem: chatItem)
|
||||
} else if ci.quotedItem == nil && ci.meta.itemForwarded == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive {
|
||||
if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) {
|
||||
EmojiItemView(chat: chat, chatItem: ci)
|
||||
} else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent {
|
||||
CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: $allowMenu)
|
||||
} else if ci.content.msgContent == nil {
|
||||
ChatItemContentView(chat: chat, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
|
||||
ChatItemContentView(chat: chat, im: im, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
|
||||
} else {
|
||||
framedItemView()
|
||||
}
|
||||
@@ -92,6 +95,7 @@ struct ChatItemView: View {
|
||||
}()
|
||||
return FramedItemView(
|
||||
chat: chat,
|
||||
im: im,
|
||||
chatItem: chatItem,
|
||||
scrollToItemId: scrollToItemId,
|
||||
preview: preview,
|
||||
@@ -108,6 +112,7 @@ struct ChatItemContentView<Content: View>: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.revealed) var revealed: Bool
|
||||
@ObservedObject var chat: Chat
|
||||
@ObservedObject var im: ItemsModel
|
||||
var chatItem: ChatItem
|
||||
var msgContentView: () -> Content
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@@ -131,7 +136,9 @@ struct ChatItemContentView<Content: View>: View {
|
||||
case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
|
||||
case .rcvDirectEvent: eventItemView()
|
||||
case .rcvGroupEvent(.memberCreatedContact): CIMemberCreatedContactView(chatItem: chatItem)
|
||||
case .rcvGroupEvent(.newMemberPendingReview): CIEventView(eventText: pendingReviewEventItemText())
|
||||
case .rcvGroupEvent: eventItemView()
|
||||
case .sndGroupEvent(.userPendingReview): CIEventView(eventText: pendingReviewEventItemText())
|
||||
case .sndGroupEvent: eventItemView()
|
||||
case .rcvConnEvent: eventItemView()
|
||||
case .sndConnEvent: eventItemView()
|
||||
@@ -140,7 +147,7 @@ struct ChatItemContentView<Content: View>: View {
|
||||
case let .rcvChatPreference(feature, allowed, param):
|
||||
CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param)
|
||||
case let .sndChatPreference(feature, _, _):
|
||||
CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary)
|
||||
CIChatFeatureView(chat: chat, im: im, chatItem: chatItem, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary)
|
||||
case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary))
|
||||
case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary))
|
||||
case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red)
|
||||
@@ -172,6 +179,13 @@ struct ChatItemContentView<Content: View>: View {
|
||||
CIEventView(eventText: eventItemViewText(theme.colors.secondary))
|
||||
}
|
||||
|
||||
private func pendingReviewEventItemText() -> Text {
|
||||
Text(chatItem.content.text)
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
|
||||
private func eventItemViewText(_ secondaryColor: Color) -> Text {
|
||||
if !revealed, let t = mergedGroupEventText {
|
||||
return chatEventText(t + textSpace + chatItem.timestampText, secondaryColor)
|
||||
@@ -187,7 +201,7 @@ struct ChatItemContentView<Content: View>: View {
|
||||
}
|
||||
|
||||
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
|
||||
CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, iconColor: iconColor)
|
||||
CIChatFeatureView(chat: chat, im: im, chatItem: chatItem, feature: feature, iconColor: iconColor)
|
||||
}
|
||||
|
||||
private var mergedGroupEventText: Text? {
|
||||
@@ -247,16 +261,17 @@ func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text {
|
||||
|
||||
struct ChatItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let im = ItemsModel.shared
|
||||
Group{
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), scrollToItemId: { _ in }).environment(\.revealed, true)
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), scrollToItemId: { _ in }).environment(\.revealed, true)
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getDeletedContentSample(), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), scrollToItemId: { _ in }).environment(\.revealed, true)
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), scrollToItemId: { _ in }).environment(\.revealed, true)
|
||||
}
|
||||
.environment(\.revealed, false)
|
||||
.previewLayout(.fixed(width: 360, height: 70))
|
||||
@@ -266,10 +281,12 @@ struct ChatItemView_Previews: PreviewProvider {
|
||||
|
||||
struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let im = ItemsModel.shared
|
||||
let ciFeatureContent = CIContent.rcvChatFeature(feature: .fullDelete, enabled: FeatureEnabled(forUser: false, forContact: false), param: nil)
|
||||
Group{
|
||||
ChatItemView(
|
||||
chat: Chat.sampleData,
|
||||
im: im,
|
||||
chatItem: ChatItem(
|
||||
chatDir: .directRcv,
|
||||
meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
|
||||
@@ -281,6 +298,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
||||
)
|
||||
ChatItemView(
|
||||
chat: Chat.sampleData,
|
||||
im: im,
|
||||
chatItem: ChatItem(
|
||||
chatDir: .directRcv,
|
||||
meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead),
|
||||
@@ -292,6 +310,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
||||
)
|
||||
ChatItemView(
|
||||
chat: Chat.sampleData,
|
||||
im: im,
|
||||
chatItem: ChatItem(
|
||||
chatDir: .directRcv,
|
||||
meta: CIMeta.getSample(1, .now, "received invitation to join group team as admin", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
|
||||
@@ -303,6 +322,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
||||
)
|
||||
ChatItemView(
|
||||
chat: Chat.sampleData,
|
||||
im: im,
|
||||
chatItem: ChatItem(
|
||||
chatDir: .directRcv,
|
||||
meta: CIMeta.getSample(1, .now, "group event text", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
|
||||
@@ -314,6 +334,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
||||
)
|
||||
ChatItemView(
|
||||
chat: Chat.sampleData,
|
||||
im: im,
|
||||
chatItem: ChatItem(
|
||||
chatDir: .directRcv,
|
||||
meta: CIMeta.getSample(1, .now, ciFeatureContent.text, .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
|
||||
|
||||
@@ -13,8 +13,8 @@ let TRIM_KEEP_COUNT = 200
|
||||
|
||||
func apiLoadMessages(
|
||||
_ chatId: ChatId,
|
||||
_ im: ItemsModel,
|
||||
_ pagination: ChatPagination,
|
||||
_ chatState: ActiveChatState,
|
||||
_ search: String = "",
|
||||
_ openAroundItemId: ChatItem.ID? = nil,
|
||||
_ visibleItemIndexesNonReversed: @MainActor () -> ClosedRange<Int> = { 0 ... 0 }
|
||||
@@ -22,7 +22,7 @@ func apiLoadMessages(
|
||||
let chat: Chat
|
||||
let navInfo: NavigationInfo
|
||||
do {
|
||||
(chat, navInfo) = try await apiGetChat(chatId: chatId, pagination: pagination, search: search)
|
||||
(chat, navInfo) = try await apiGetChat(chatId: chatId, scope: im.groupScopeInfo?.toChatScope(), contentTag: im.contentTag, pagination: pagination, search: search)
|
||||
} catch let error {
|
||||
logger.error("apiLoadMessages error: \(responseError(error))")
|
||||
return
|
||||
@@ -38,30 +38,31 @@ func apiLoadMessages(
|
||||
return
|
||||
}
|
||||
|
||||
let unreadAfterItemId = chatState.unreadAfterItemId
|
||||
let unreadAfterItemId = im.chatState.unreadAfterItemId
|
||||
|
||||
let oldItems = Array(ItemsModel.shared.reversedChatItems.reversed())
|
||||
let oldItems = Array(im.reversedChatItems.reversed())
|
||||
var newItems: [ChatItem] = []
|
||||
switch pagination {
|
||||
case .initial:
|
||||
let newSplits: [Int64] = if !chat.chatItems.isEmpty && navInfo.afterTotal > 0 { [chat.chatItems.last!.id] } else { [] }
|
||||
if chatModel.getChat(chat.id) == nil {
|
||||
if im.secondaryIMFilter == nil && chatModel.getChat(chat.id) == nil {
|
||||
chatModel.addChat(chat)
|
||||
}
|
||||
await MainActor.run {
|
||||
chatModel.chatItemStatuses.removeAll()
|
||||
ItemsModel.shared.reversedChatItems = chat.chatItems.reversed()
|
||||
chatModel.updateChatInfo(chat.chatInfo)
|
||||
chatState.splits = newSplits
|
||||
if !chat.chatItems.isEmpty {
|
||||
chatState.unreadAfterItemId = chat.chatItems.last!.id
|
||||
im.reversedChatItems = chat.chatItems.reversed()
|
||||
if im.secondaryIMFilter == nil {
|
||||
chatModel.updateChatInfo(chat.chatInfo)
|
||||
}
|
||||
chatState.totalAfter = navInfo.afterTotal
|
||||
chatState.unreadTotal = chat.chatStats.unreadCount
|
||||
chatState.unreadAfter = navInfo.afterUnread
|
||||
chatState.unreadAfterNewestLoaded = navInfo.afterUnread
|
||||
im.chatState.splits = newSplits
|
||||
if !chat.chatItems.isEmpty {
|
||||
im.chatState.unreadAfterItemId = chat.chatItems.last!.id
|
||||
}
|
||||
im.chatState.totalAfter = navInfo.afterTotal
|
||||
im.chatState.unreadTotal = chat.chatStats.unreadCount
|
||||
im.chatState.unreadAfter = navInfo.afterUnread
|
||||
im.chatState.unreadAfterNewestLoaded = navInfo.afterUnread
|
||||
|
||||
PreloadState.shared.clear()
|
||||
im.preloadState.clear()
|
||||
}
|
||||
case let .before(paginationChatItemId, _):
|
||||
newItems.append(contentsOf: oldItems)
|
||||
@@ -71,15 +72,15 @@ func apiLoadMessages(
|
||||
let wasSize = newItems.count
|
||||
let visibleItemIndexes = await MainActor.run { visibleItemIndexesNonReversed() }
|
||||
let modifiedSplits = removeDuplicatesAndModifySplitsOnBeforePagination(
|
||||
unreadAfterItemId, &newItems, newIds, chatState.splits, visibleItemIndexes
|
||||
unreadAfterItemId, &newItems, newIds, im.chatState.splits, visibleItemIndexes
|
||||
)
|
||||
let insertAt = max((indexInCurrentItems - (wasSize - newItems.count) + modifiedSplits.trimmedIds.count), 0)
|
||||
newItems.insert(contentsOf: chat.chatItems, at: insertAt)
|
||||
let newReversed: [ChatItem] = newItems.reversed()
|
||||
await MainActor.run {
|
||||
ItemsModel.shared.reversedChatItems = newReversed
|
||||
chatState.splits = modifiedSplits.newSplits
|
||||
chatState.moveUnreadAfterItem(modifiedSplits.oldUnreadSplitIndex, modifiedSplits.newUnreadSplitIndex, oldItems)
|
||||
im.reversedChatItems = newReversed
|
||||
im.chatState.splits = modifiedSplits.newSplits
|
||||
im.chatState.moveUnreadAfterItem(modifiedSplits.oldUnreadSplitIndex, modifiedSplits.newUnreadSplitIndex, oldItems)
|
||||
}
|
||||
case let .after(paginationChatItemId, _):
|
||||
newItems.append(contentsOf: oldItems)
|
||||
@@ -89,7 +90,7 @@ func apiLoadMessages(
|
||||
let mappedItems = mapItemsToIds(chat.chatItems)
|
||||
let newIds = mappedItems.0
|
||||
let (newSplits, unreadInLoaded) = removeDuplicatesAndModifySplitsOnAfterPagination(
|
||||
mappedItems.1, paginationChatItemId, &newItems, newIds, chat, chatState.splits
|
||||
mappedItems.1, paginationChatItemId, &newItems, newIds, chat, im.chatState.splits
|
||||
)
|
||||
let indexToAdd = min(indexInCurrentItems + 1, newItems.count)
|
||||
let indexToAddIsLast = indexToAdd == newItems.count
|
||||
@@ -97,19 +98,19 @@ func apiLoadMessages(
|
||||
let new: [ChatItem] = newItems
|
||||
let newReversed: [ChatItem] = newItems.reversed()
|
||||
await MainActor.run {
|
||||
ItemsModel.shared.reversedChatItems = newReversed
|
||||
chatState.splits = newSplits
|
||||
chatState.moveUnreadAfterItem(chatState.splits.first ?? new.last!.id, new)
|
||||
im.reversedChatItems = newReversed
|
||||
im.chatState.splits = newSplits
|
||||
im.chatState.moveUnreadAfterItem(im.chatState.splits.first ?? new.last!.id, new)
|
||||
// loading clear bottom area, updating number of unread items after the newest loaded item
|
||||
if indexToAddIsLast {
|
||||
chatState.unreadAfterNewestLoaded -= unreadInLoaded
|
||||
im.chatState.unreadAfterNewestLoaded -= unreadInLoaded
|
||||
}
|
||||
}
|
||||
case .around:
|
||||
var newSplits: [Int64]
|
||||
if openAroundItemId == nil {
|
||||
newItems.append(contentsOf: oldItems)
|
||||
newSplits = await removeDuplicatesAndUpperSplits(&newItems, chat, chatState.splits, visibleItemIndexesNonReversed)
|
||||
newSplits = await removeDuplicatesAndUpperSplits(&newItems, chat, im.chatState.splits, visibleItemIndexesNonReversed)
|
||||
} else {
|
||||
newSplits = []
|
||||
}
|
||||
@@ -120,33 +121,37 @@ func apiLoadMessages(
|
||||
let newReversed: [ChatItem] = newItems.reversed()
|
||||
let orderedSplits = newSplits
|
||||
await MainActor.run {
|
||||
ItemsModel.shared.reversedChatItems = newReversed
|
||||
chatState.splits = orderedSplits
|
||||
chatState.unreadAfterItemId = chat.chatItems.last!.id
|
||||
chatState.totalAfter = navInfo.afterTotal
|
||||
chatState.unreadTotal = chat.chatStats.unreadCount
|
||||
chatState.unreadAfter = navInfo.afterUnread
|
||||
im.reversedChatItems = newReversed
|
||||
im.chatState.splits = orderedSplits
|
||||
im.chatState.unreadAfterItemId = chat.chatItems.last!.id
|
||||
im.chatState.totalAfter = navInfo.afterTotal
|
||||
im.chatState.unreadTotal = chat.chatStats.unreadCount
|
||||
im.chatState.unreadAfter = navInfo.afterUnread
|
||||
|
||||
if let openAroundItemId {
|
||||
chatState.unreadAfterNewestLoaded = navInfo.afterUnread
|
||||
ChatModel.shared.openAroundItemId = openAroundItemId
|
||||
ChatModel.shared.chatId = chatId
|
||||
im.chatState.unreadAfterNewestLoaded = navInfo.afterUnread
|
||||
if im.secondaryIMFilter == nil {
|
||||
ChatModel.shared.openAroundItemId = openAroundItemId // TODO [knocking] move openAroundItemId from ChatModel to ItemsModel?
|
||||
ChatModel.shared.chatId = chat.id
|
||||
}
|
||||
} else {
|
||||
// no need to set it, count will be wrong
|
||||
// chatState.unreadAfterNewestLoaded = navInfo.afterUnread
|
||||
}
|
||||
PreloadState.shared.clear()
|
||||
im.preloadState.clear()
|
||||
}
|
||||
case .last:
|
||||
newItems.append(contentsOf: oldItems)
|
||||
let newSplits = await removeDuplicatesAndUnusedSplits(&newItems, chat, chatState.splits)
|
||||
let newSplits = await removeDuplicatesAndUnusedSplits(&newItems, chat, im.chatState.splits)
|
||||
newItems.append(contentsOf: chat.chatItems)
|
||||
let items = newItems
|
||||
await MainActor.run {
|
||||
ItemsModel.shared.reversedChatItems = items.reversed()
|
||||
chatState.splits = newSplits
|
||||
chatModel.updateChatInfo(chat.chatInfo)
|
||||
chatState.unreadAfterNewestLoaded = 0
|
||||
im.reversedChatItems = items.reversed()
|
||||
im.chatState.splits = newSplits
|
||||
if im.secondaryIMFilter == nil {
|
||||
chatModel.updateChatInfo(chat.chatInfo)
|
||||
}
|
||||
im.chatState.unreadAfterNewestLoaded = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct MergedItems: Hashable, Equatable {
|
||||
let im: ItemsModel
|
||||
let items: [MergedItem]
|
||||
let splits: [SplitRange]
|
||||
// chat item id, index in list
|
||||
@@ -23,15 +24,15 @@ struct MergedItems: Hashable, Equatable {
|
||||
hasher.combine("\(items.hashValue)")
|
||||
}
|
||||
|
||||
static func create(_ items: [ChatItem], _ revealedItems: Set<Int64>, _ chatState: ActiveChatState) -> MergedItems {
|
||||
if items.isEmpty {
|
||||
return MergedItems(items: [], splits: [], indexInParentItems: [:])
|
||||
static func create(_ im: ItemsModel, _ revealedItems: Set<Int64>) -> MergedItems {
|
||||
if im.reversedChatItems.isEmpty {
|
||||
return MergedItems(im: im, items: [], splits: [], indexInParentItems: [:])
|
||||
}
|
||||
|
||||
let unreadCount = chatState.unreadTotal
|
||||
let unreadCount = im.chatState.unreadTotal
|
||||
|
||||
let unreadAfterItemId = chatState.unreadAfterItemId
|
||||
let itemSplits = chatState.splits
|
||||
let unreadAfterItemId = im.chatState.unreadAfterItemId
|
||||
let itemSplits = im.chatState.splits
|
||||
var mergedItems: [MergedItem] = []
|
||||
// Indexes of splits here will be related to reversedChatItems, not chatModel.chatItems
|
||||
var splitRanges: [SplitRange] = []
|
||||
@@ -40,19 +41,19 @@ struct MergedItems: Hashable, Equatable {
|
||||
var unclosedSplitIndex: Int? = nil
|
||||
var unclosedSplitIndexInParent: Int? = nil
|
||||
var visibleItemIndexInParent = -1
|
||||
var unreadBefore = unreadCount - chatState.unreadAfterNewestLoaded
|
||||
var unreadBefore = unreadCount - im.chatState.unreadAfterNewestLoaded
|
||||
var lastRevealedIdsInMergedItems: BoxedValue<[Int64]>? = nil
|
||||
var lastRangeInReversedForMergedItems: BoxedValue<ClosedRange<Int>>? = nil
|
||||
var recent: MergedItem? = nil
|
||||
while index < items.count {
|
||||
let item = items[index]
|
||||
let prev = index >= 1 ? items[index - 1] : nil
|
||||
let next = index + 1 < items.count ? items[index + 1] : nil
|
||||
while index < im.reversedChatItems.count {
|
||||
let item = im.reversedChatItems[index]
|
||||
let prev = index >= 1 ? im.reversedChatItems[index - 1] : nil
|
||||
let next = index + 1 < im.reversedChatItems.count ? im.reversedChatItems[index + 1] : nil
|
||||
let category = item.mergeCategory
|
||||
let itemIsSplit = itemSplits.contains(item.id)
|
||||
|
||||
if item.id == unreadAfterItemId {
|
||||
unreadBefore = unreadCount - chatState.unreadAfter
|
||||
unreadBefore = unreadCount - im.chatState.unreadAfter
|
||||
}
|
||||
if item.isRcvNew {
|
||||
unreadBefore -= 1
|
||||
@@ -106,18 +107,19 @@ struct MergedItems: Hashable, Equatable {
|
||||
// found item that is considered as a split
|
||||
if let unclosedSplitIndex, let unclosedSplitIndexInParent {
|
||||
// it was at least second split in the list
|
||||
splitRanges.append(SplitRange(itemId: items[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index - 1, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent - 1))
|
||||
splitRanges.append(SplitRange(itemId: im.reversedChatItems[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index - 1, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent - 1))
|
||||
}
|
||||
unclosedSplitIndex = index
|
||||
unclosedSplitIndexInParent = visibleItemIndexInParent
|
||||
} else if index + 1 == items.count, let unclosedSplitIndex, let unclosedSplitIndexInParent {
|
||||
} else if index + 1 == im.reversedChatItems.count, let unclosedSplitIndex, let unclosedSplitIndexInParent {
|
||||
// just one split for the whole list, there will be no more, it's the end
|
||||
splitRanges.append(SplitRange(itemId: items[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent))
|
||||
splitRanges.append(SplitRange(itemId: im.reversedChatItems[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent))
|
||||
}
|
||||
indexInParentItems[item.id] = visibleItemIndexInParent
|
||||
index += 1
|
||||
}
|
||||
return MergedItems(
|
||||
im: im,
|
||||
items: mergedItems,
|
||||
splits: splitRanges,
|
||||
indexInParentItems: indexInParentItems
|
||||
@@ -127,7 +129,6 @@ struct MergedItems: Hashable, Equatable {
|
||||
// Use this check to ensure that mergedItems state based on currently actual state of global
|
||||
// splits and reversedChatItems
|
||||
func isActualState() -> Bool {
|
||||
let im = ItemsModel.shared
|
||||
// do not load anything if global splits state is different than in merged items because it
|
||||
// will produce undefined results in terms of loading and placement of items.
|
||||
// Same applies to reversedChatItems
|
||||
@@ -434,7 +435,7 @@ class BoxedValue<T: Hashable>: Equatable, Hashable {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func visibleItemIndexesNonReversed(_ listState: EndlessScrollView<MergedItem>.ListState, _ mergedItems: MergedItems) -> ClosedRange<Int> {
|
||||
func visibleItemIndexesNonReversed(_ im: ItemsModel, _ listState: EndlessScrollView<MergedItem>.ListState, _ mergedItems: MergedItems) -> ClosedRange<Int> {
|
||||
let zero = 0 ... 0
|
||||
let items = mergedItems.items
|
||||
if items.isEmpty {
|
||||
@@ -445,12 +446,12 @@ func visibleItemIndexesNonReversed(_ listState: EndlessScrollView<MergedItem>.Li
|
||||
guard let newest, let oldest else {
|
||||
return zero
|
||||
}
|
||||
let size = ItemsModel.shared.reversedChatItems.count
|
||||
let size = im.reversedChatItems.count
|
||||
let range = size - oldest ... size - newest
|
||||
if range.lowerBound < 0 || range.upperBound < 0 {
|
||||
return zero
|
||||
}
|
||||
|
||||
// visible items mapped to their underlying data structure which is ItemsModel.shared.reversedChatItems.reversed()
|
||||
// visible items mapped to their underlying data structure which is im.reversedChatItems.reversed()
|
||||
return range
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
func loadLastItems(_ loadingMoreItems: Binding<Bool>, loadingBottomItems: Binding<Bool>, _ chat: Chat) async {
|
||||
func loadLastItems(_ loadingMoreItems: Binding<Bool>, loadingBottomItems: Binding<Bool>, _ chat: Chat, _ im: ItemsModel) async {
|
||||
await MainActor.run {
|
||||
loadingMoreItems.wrappedValue = true
|
||||
loadingBottomItems.wrappedValue = true
|
||||
@@ -22,27 +22,15 @@ func loadLastItems(_ loadingMoreItems: Binding<Bool>, loadingBottomItems: Bindin
|
||||
}
|
||||
return
|
||||
}
|
||||
await apiLoadMessages(chat.chatInfo.id, ChatPagination.last(count: 50), ItemsModel.shared.chatState)
|
||||
await apiLoadMessages(chat.chatInfo.id, im, ChatPagination.last(count: 50))
|
||||
await MainActor.run {
|
||||
loadingMoreItems.wrappedValue = false
|
||||
loadingBottomItems.wrappedValue = false
|
||||
}
|
||||
}
|
||||
|
||||
class PreloadState {
|
||||
static let shared = PreloadState()
|
||||
var prevFirstVisible: Int64 = Int64.min
|
||||
var prevItemsCount: Int = 0
|
||||
var preloading: Bool = false
|
||||
|
||||
func clear() {
|
||||
prevFirstVisible = Int64.min
|
||||
prevItemsCount = 0
|
||||
preloading = false
|
||||
}
|
||||
}
|
||||
|
||||
func preloadIfNeeded(
|
||||
_ im: ItemsModel,
|
||||
_ allowLoadMoreItems: Binding<Bool>,
|
||||
_ ignoreLoadingRequests: Binding<Int64?>,
|
||||
_ listState: EndlessScrollView<MergedItem>.ListState,
|
||||
@@ -50,7 +38,7 @@ func preloadIfNeeded(
|
||||
loadItems: @escaping (Bool, ChatPagination) async -> Bool,
|
||||
loadLastItems: @escaping () async -> Void
|
||||
) {
|
||||
let state = PreloadState.shared
|
||||
let state = im.preloadState
|
||||
guard !listState.isScrolling && !listState.isAnimatedScrolling,
|
||||
!state.preloading,
|
||||
listState.totalItemsCount > 0
|
||||
@@ -63,7 +51,7 @@ func preloadIfNeeded(
|
||||
Task {
|
||||
defer { state.preloading = false }
|
||||
var triedToLoad = true
|
||||
await preloadItems(mergedItems.boxedValue, allowLoadMore, listState, ignoreLoadingRequests) { pagination in
|
||||
await preloadItems(im, mergedItems.boxedValue, allowLoadMore, listState, ignoreLoadingRequests) { pagination in
|
||||
triedToLoad = await loadItems(false, pagination)
|
||||
return triedToLoad
|
||||
}
|
||||
@@ -73,11 +61,11 @@ func preloadIfNeeded(
|
||||
}
|
||||
// it's important to ask last items when the view is fully covered with items. Otherwise, visible items from one
|
||||
// split will be merged with last items and position of scroll will change unexpectedly.
|
||||
if listState.itemsCanCoverScreen && !ItemsModel.shared.lastItemsLoaded {
|
||||
if listState.itemsCanCoverScreen && !im.lastItemsLoaded {
|
||||
await loadLastItems()
|
||||
}
|
||||
}
|
||||
} else if listState.itemsCanCoverScreen && !ItemsModel.shared.lastItemsLoaded {
|
||||
} else if listState.itemsCanCoverScreen && !im.lastItemsLoaded {
|
||||
state.preloading = true
|
||||
Task {
|
||||
defer { state.preloading = false }
|
||||
@@ -87,6 +75,7 @@ func preloadIfNeeded(
|
||||
}
|
||||
|
||||
func preloadItems(
|
||||
_ im: ItemsModel,
|
||||
_ mergedItems: MergedItems,
|
||||
_ allowLoadMoreItems: Bool,
|
||||
_ listState: EndlessScrollView<MergedItem>.ListState,
|
||||
@@ -105,7 +94,7 @@ async {
|
||||
let splits = mergedItems.splits
|
||||
let lastVisibleIndex = listState.lastVisibleItemIndex
|
||||
var lastIndexToLoadFrom: Int? = findLastIndexToLoadFromInSplits(firstVisibleIndex, lastVisibleIndex, remaining, splits)
|
||||
let items: [ChatItem] = ItemsModel.shared.reversedChatItems.reversed()
|
||||
let items: [ChatItem] = im.reversedChatItems.reversed()
|
||||
if splits.isEmpty && !items.isEmpty && lastVisibleIndex > mergedItems.items.count - remaining {
|
||||
lastIndexToLoadFrom = items.count - 1
|
||||
}
|
||||
@@ -122,7 +111,7 @@ async {
|
||||
let sizeWas = items.count
|
||||
let firstItemIdWas = items.first?.id
|
||||
let triedToLoad = await loadItems(ChatPagination.before(chatItemId: loadFromItemId, count: ChatPagination.PRELOAD_COUNT))
|
||||
if triedToLoad && sizeWas == ItemsModel.shared.reversedChatItems.count && firstItemIdWas == ItemsModel.shared.reversedChatItems.last?.id {
|
||||
if triedToLoad && sizeWas == im.reversedChatItems.count && firstItemIdWas == im.reversedChatItems.last?.id {
|
||||
ignoreLoadingRequests.wrappedValue = loadFromItemId
|
||||
return false
|
||||
}
|
||||
@@ -133,7 +122,7 @@ async {
|
||||
let splits = mergedItems.splits
|
||||
let split = splits.last(where: { $0.indexRangeInParentItems.contains(firstVisibleIndex) })
|
||||
// we're inside a splitRange (top --- [end of the splitRange --- we're here --- start of the splitRange] --- bottom)
|
||||
let reversedItems: [ChatItem] = ItemsModel.shared.reversedChatItems
|
||||
let reversedItems: [ChatItem] = im.reversedChatItems
|
||||
if let split, split.indexRangeInParentItems.lowerBound + remaining > firstVisibleIndex {
|
||||
let index = split.indexRangeInReversed.lowerBound
|
||||
if index >= 0 {
|
||||
|
||||
@@ -15,8 +15,6 @@ private let memberImageSize: CGFloat = 34
|
||||
|
||||
struct ChatView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ObservedObject var im = ItemsModel.shared
|
||||
@State var mergedItems: BoxedValue<MergedItems> = BoxedValue(MergedItems.create(ItemsModel.shared.reversedChatItems, [], ItemsModel.shared.chatState))
|
||||
@State var revealedItems: Set<Int64> = Set()
|
||||
@State var theme: AppTheme = buildTheme()
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@@ -24,6 +22,9 @@ struct ChatView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@State @ObservedObject var chat: Chat
|
||||
@ObservedObject var im: ItemsModel
|
||||
@State var mergedItems: BoxedValue<MergedItems>
|
||||
@State var floatingButtonModel: FloatingButtonModel
|
||||
@State private var showChatInfoSheet: Bool = false
|
||||
@State private var showAddMembersSheet: Bool = false
|
||||
@State private var composeState = ComposeState()
|
||||
@@ -55,7 +56,6 @@ struct ChatView: View {
|
||||
@State private var allowLoadMoreItems: Bool = false
|
||||
@State private var ignoreLoadingRequests: Int64? = nil
|
||||
@State private var animatedScrollingInProgress: Bool = false
|
||||
@State private var floatingButtonModel: FloatingButtonModel = FloatingButtonModel()
|
||||
|
||||
@State private var scrollView: EndlessScrollView<MergedItem> = EndlessScrollView(frame: .zero)
|
||||
|
||||
@@ -80,7 +80,7 @@ struct ChatView: View {
|
||||
let backgroundColor = theme.wallpaper.background ?? wallpaperType.defaultBackgroundColor(theme.base, theme.colors.background)
|
||||
let tintColor = theme.wallpaper.tint ?? wallpaperType.defaultTintColor(theme.base)
|
||||
Color.clear.ignoresSafeArea(.all)
|
||||
.if(wallpaperImage != nil) { view in
|
||||
.if(wallpaperImage != nil && im.secondaryIMFilter == nil) { view in
|
||||
view.modifier(
|
||||
ChatViewBackground(image: wallpaperImage!, imageType: wallpaperType, background: backgroundColor, tint: tintColor)
|
||||
)
|
||||
@@ -91,8 +91,8 @@ struct ChatView: View {
|
||||
if let groupInfo = chat.chatInfo.groupInfo, !composeState.message.isEmpty {
|
||||
GroupMentionsView(groupInfo: groupInfo, composeState: $composeState, selectedRange: $selectedRange, keyboardVisible: $keyboardVisible)
|
||||
}
|
||||
FloatingButtons(theme: theme, scrollView: scrollView, chat: chat, loadingMoreItems: $loadingMoreItems, loadingTopItems: $loadingTopItems, requestedTopScroll: $requestedTopScroll, loadingBottomItems: $loadingBottomItems, requestedBottomScroll: $requestedBottomScroll, animatedScrollingInProgress: $animatedScrollingInProgress, listState: scrollView.listState, model: floatingButtonModel, reloadItems: {
|
||||
mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState)
|
||||
FloatingButtons(im: im, theme: theme, scrollView: scrollView, chat: chat, loadingMoreItems: $loadingMoreItems, loadingTopItems: $loadingTopItems, requestedTopScroll: $requestedTopScroll, loadingBottomItems: $loadingBottomItems, requestedBottomScroll: $requestedBottomScroll, animatedScrollingInProgress: $animatedScrollingInProgress, listState: scrollView.listState, model: floatingButtonModel, reloadItems: {
|
||||
mergedItems.boxedValue = MergedItems.create(im, revealedItems)
|
||||
scrollView.updateItems(mergedItems.boxedValue.items)
|
||||
}
|
||||
)
|
||||
@@ -101,6 +101,7 @@ struct ChatView: View {
|
||||
if selectedChatItems == nil {
|
||||
ComposeView(
|
||||
chat: chat,
|
||||
im: im,
|
||||
composeState: $composeState,
|
||||
keyboardVisible: $keyboardVisible,
|
||||
keyboardHiddenDate: $keyboardHiddenDate,
|
||||
@@ -109,7 +110,7 @@ struct ChatView: View {
|
||||
.disabled(!cInfo.sendMsgEnabled)
|
||||
} else {
|
||||
SelectedItemsBottomToolbar(
|
||||
chatItems: ItemsModel.shared.reversedChatItems,
|
||||
im: im,
|
||||
selectedChatItems: $selectedChatItems,
|
||||
chatInfo: chat.chatInfo,
|
||||
deleteItems: { forAll in
|
||||
@@ -120,7 +121,7 @@ struct ChatView: View {
|
||||
showArchiveSelectedReports = true
|
||||
},
|
||||
moderateItems: {
|
||||
if case let .group(groupInfo) = chat.chatInfo {
|
||||
if case let .group(groupInfo, _) = chat.chatInfo {
|
||||
showModerateSelectedMessagesAlert(groupInfo)
|
||||
}
|
||||
},
|
||||
@@ -163,7 +164,7 @@ struct ChatView: View {
|
||||
archiveReports(chat.chatInfo, 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.chatInfo, selected.sorted(), true, deletedSelectedMessages)
|
||||
@@ -173,7 +174,7 @@ struct ChatView: View {
|
||||
}
|
||||
.appSheet(item: $selectedMember) { member in
|
||||
Group {
|
||||
if case let .group(groupInfo) = chat.chatInfo {
|
||||
if case let .group(groupInfo, _) = chat.chatInfo {
|
||||
GroupMemberInfoView(
|
||||
groupInfo: groupInfo,
|
||||
chat: chat,
|
||||
@@ -186,7 +187,7 @@ 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) {
|
||||
Group {
|
||||
if case let .group(groupInfo) = cInfo {
|
||||
if case let .group(groupInfo, _) = cInfo {
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
|
||||
}
|
||||
}
|
||||
@@ -236,7 +237,7 @@ struct ChatView: View {
|
||||
initChatView()
|
||||
theme = buildTheme()
|
||||
closeSearch()
|
||||
mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState)
|
||||
mergedItems.boxedValue = MergedItems.create(im, revealedItems)
|
||||
scrollView.updateItems(mergedItems.boxedValue.items)
|
||||
|
||||
if let openAround = chatModel.openAroundItemId, let index = mergedItems.boxedValue.indexInParentItems[openAround] {
|
||||
@@ -256,7 +257,7 @@ struct ChatView: View {
|
||||
.onChange(of: chatModel.openAroundItemId) { openAround in
|
||||
if let openAround {
|
||||
closeSearch()
|
||||
mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState)
|
||||
mergedItems.boxedValue = MergedItems.create(im, revealedItems)
|
||||
scrollView.updateItems(mergedItems.boxedValue.items)
|
||||
chatModel.openAroundItemId = nil
|
||||
|
||||
@@ -279,9 +280,8 @@ struct ChatView: View {
|
||||
if chatModel.chatId == cInfo.id && !presentationMode.wrappedValue.isPresented {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
|
||||
if chatModel.chatId == nil {
|
||||
chatModel.chatItemStatuses = [:]
|
||||
ItemsModel.shared.reversedChatItems = []
|
||||
ItemsModel.shared.chatState.clear()
|
||||
im.reversedChatItems = []
|
||||
im.chatState.clear()
|
||||
chatModel.groupMembers = []
|
||||
chatModel.groupMembersIndexes.removeAll()
|
||||
chatModel.membersLoaded = false
|
||||
@@ -292,124 +292,178 @@ struct ChatView: View {
|
||||
.onChange(of: colorScheme) { _ in
|
||||
theme = buildTheme()
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
if selectedChatItems != nil {
|
||||
SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems)
|
||||
} else if case let .direct(contact) = cInfo {
|
||||
Button {
|
||||
Task {
|
||||
showChatInfoSheet = true
|
||||
}
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
}
|
||||
.appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) {
|
||||
ChatInfoView(
|
||||
chat: chat,
|
||||
contact: contact,
|
||||
localAlias: chat.chatInfo.localAlias,
|
||||
featuresAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences),
|
||||
currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences),
|
||||
onSearch: { focusSearch() }
|
||||
)
|
||||
}
|
||||
} else if case let .group(groupInfo) = cInfo {
|
||||
Button {
|
||||
Task { await chatModel.loadGroupMembers(groupInfo) { showChatInfoSheet = true } }
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
.appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) {
|
||||
GroupChatInfoView(
|
||||
chat: chat,
|
||||
groupInfo: Binding(
|
||||
get: { groupInfo },
|
||||
set: { gInfo in
|
||||
chat.chatInfo = .group(groupInfo: gInfo)
|
||||
chat.created = Date.now
|
||||
}
|
||||
),
|
||||
onSearch: { focusSearch() },
|
||||
localAlias: groupInfo.localAlias
|
||||
)
|
||||
}
|
||||
} else if case .local = cInfo {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if selectedChatItems != nil {
|
||||
Button {
|
||||
withAnimation {
|
||||
selectedChatItems = nil
|
||||
}
|
||||
} label: {
|
||||
Text("Cancel")
|
||||
}
|
||||
} else {
|
||||
switch cInfo {
|
||||
case let .direct(contact):
|
||||
HStack {
|
||||
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
|
||||
if callsPrefEnabled {
|
||||
if chatModel.activeCall == nil {
|
||||
callButton(contact, .audio, imageName: "phone")
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
} else if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
||||
endCallButton(call)
|
||||
}
|
||||
.if(im.secondaryIMFilter == nil) {
|
||||
$0.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
if selectedChatItems != nil {
|
||||
SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems)
|
||||
} else if case let .direct(contact) = cInfo {
|
||||
Button {
|
||||
Task {
|
||||
showChatInfoSheet = true
|
||||
}
|
||||
Menu {
|
||||
if callsPrefEnabled && chatModel.activeCall == nil {
|
||||
Button {
|
||||
CallController.shared.startCall(contact, .video)
|
||||
} label: {
|
||||
Label("Video call", systemImage: "video")
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
}
|
||||
.appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) {
|
||||
ChatInfoView(
|
||||
chat: chat,
|
||||
contact: contact,
|
||||
localAlias: chat.chatInfo.localAlias,
|
||||
featuresAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences),
|
||||
currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences),
|
||||
onSearch: { focusSearch() }
|
||||
)
|
||||
}
|
||||
} else if case let .group(groupInfo, _) = cInfo {
|
||||
Button {
|
||||
Task { await chatModel.loadGroupMembers(groupInfo) { showChatInfoSheet = true } }
|
||||
} label: {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
.appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) {
|
||||
GroupChatInfoView(
|
||||
chat: chat,
|
||||
groupInfo: Binding(
|
||||
get: { groupInfo },
|
||||
set: { gInfo in
|
||||
chat.chatInfo = .group(groupInfo: gInfo, groupChatScope: nil)
|
||||
chat.created = Date.now
|
||||
}
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
}
|
||||
searchButton()
|
||||
ToggleNtfsButton(chat: chat)
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
}
|
||||
),
|
||||
onSearch: { focusSearch() },
|
||||
localAlias: groupInfo.localAlias
|
||||
)
|
||||
}
|
||||
case let .group(groupInfo):
|
||||
HStack {
|
||||
if groupInfo.canAddMembers {
|
||||
if (chat.chatInfo.incognito) {
|
||||
groupLinkButton()
|
||||
.appSheet(isPresented: $showGroupLinkSheet) {
|
||||
GroupLinkView(
|
||||
groupId: groupInfo.groupId,
|
||||
groupLink: $groupLink,
|
||||
groupLinkMemberRole: $groupLinkMemberRole,
|
||||
showTitle: true,
|
||||
creatingGroup: false
|
||||
)
|
||||
} else if case .local = cInfo {
|
||||
ChatInfoToolbar(chat: chat)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if selectedChatItems != nil {
|
||||
Button {
|
||||
withAnimation {
|
||||
selectedChatItems = nil
|
||||
}
|
||||
} label: {
|
||||
Text("Cancel")
|
||||
}
|
||||
} else {
|
||||
switch cInfo {
|
||||
case let .direct(contact):
|
||||
HStack {
|
||||
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
|
||||
if callsPrefEnabled {
|
||||
if chatModel.activeCall == nil {
|
||||
callButton(contact, .audio, imageName: "phone")
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
} else if let call = chatModel.activeCall, call.contact.id == cInfo.id {
|
||||
endCallButton(call)
|
||||
}
|
||||
}
|
||||
Menu {
|
||||
if callsPrefEnabled && chatModel.activeCall == nil {
|
||||
Button {
|
||||
CallController.shared.startCall(contact, .video)
|
||||
} label: {
|
||||
Label("Video call", systemImage: "video")
|
||||
}
|
||||
} else {
|
||||
addMembersButton()
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
}
|
||||
searchButton()
|
||||
ToggleNtfsButton(chat: chat)
|
||||
.disabled(!contact.ready || !contact.active)
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
}
|
||||
}
|
||||
Menu {
|
||||
searchButton()
|
||||
ToggleNtfsButton(chat: chat)
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
case let .group(groupInfo, _):
|
||||
HStack {
|
||||
if groupInfo.canAddMembers {
|
||||
if (chat.chatInfo.incognito) {
|
||||
groupLinkButton()
|
||||
.appSheet(isPresented: $showGroupLinkSheet) {
|
||||
GroupLinkView(
|
||||
groupId: groupInfo.groupId,
|
||||
groupLink: $groupLink,
|
||||
groupLinkMemberRole: $groupLinkMemberRole,
|
||||
showTitle: true,
|
||||
creatingGroup: false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
addMembersButton()
|
||||
}
|
||||
}
|
||||
Menu {
|
||||
searchButton()
|
||||
ToggleNtfsButton(chat: chat)
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
}
|
||||
}
|
||||
case .local:
|
||||
searchButton()
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
case .local:
|
||||
searchButton()
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.if(im.secondaryIMFilter != nil) {
|
||||
$0.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
if selectedChatItems != nil {
|
||||
SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems)
|
||||
} else {
|
||||
switch im.secondaryIMFilter {
|
||||
case let .groupChatScopeContext(groupScopeInfo):
|
||||
switch groupScopeInfo {
|
||||
case let .memberSupport(groupMember_):
|
||||
if let groupMember = groupMember_ {
|
||||
MemberSupportChatToolbar(groupMember: groupMember)
|
||||
} else {
|
||||
textChatToolbar("Support")
|
||||
}
|
||||
}
|
||||
case let .msgContentTagContext(contentTag):
|
||||
switch contentTag {
|
||||
case .report:
|
||||
textChatToolbar("Member reports")
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
case .none:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if selectedChatItems != nil {
|
||||
Button {
|
||||
withAnimation {
|
||||
selectedChatItems = nil
|
||||
}
|
||||
} label: {
|
||||
Text("Cancel")
|
||||
}
|
||||
} else {
|
||||
searchButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func textChatToolbar(_ text: LocalizedStringKey) -> some View {
|
||||
HStack {
|
||||
Text(text).font(.headline)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
.frame(width: 220)
|
||||
}
|
||||
|
||||
private func initChatView() {
|
||||
@@ -448,13 +502,13 @@ struct ChatView: View {
|
||||
var index = mergedItems.boxedValue.indexInParentItems[itemId]
|
||||
if index == nil {
|
||||
let pagination = ChatPagination.around(chatItemId: itemId, count: ChatPagination.PRELOAD_COUNT * 2)
|
||||
let oldSize = ItemsModel.shared.reversedChatItems.count
|
||||
let oldSize = im.reversedChatItems.count
|
||||
let triedToLoad = await loadChatItems(chat, pagination)
|
||||
if !triedToLoad {
|
||||
return
|
||||
}
|
||||
var repeatsLeft = 50
|
||||
while oldSize == ItemsModel.shared.reversedChatItems.count && repeatsLeft > 0 {
|
||||
while oldSize == im.reversedChatItems.count && repeatsLeft > 0 {
|
||||
try await Task.sleep(nanoseconds: 20_000000)
|
||||
repeatsLeft -= 1
|
||||
}
|
||||
@@ -464,7 +518,7 @@ struct ChatView: View {
|
||||
closeKeyboardAndRun {
|
||||
Task {
|
||||
await MainActor.run { animatedScrollingInProgress = true }
|
||||
await scrollView.scrollToItemAnimated(min(ItemsModel.shared.reversedChatItems.count - 1, index))
|
||||
await scrollView.scrollToItemAnimated(min(im.reversedChatItems.count - 1, index))
|
||||
await MainActor.run { animatedScrollingInProgress = false }
|
||||
}
|
||||
}
|
||||
@@ -539,6 +593,7 @@ struct ChatView: View {
|
||||
? (g.size.width - 32)
|
||||
: (g.size.width - 32) * 0.84
|
||||
return ChatItemWithMenu(
|
||||
im: im,
|
||||
chat: $chat,
|
||||
index: index,
|
||||
isLastItem: index == mergedItems.boxedValue.items.count - 1,
|
||||
@@ -571,7 +626,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
.onChange(of: im.reversedChatItems) { items in
|
||||
mergedItems.boxedValue = MergedItems.create(items, revealedItems, im.chatState)
|
||||
mergedItems.boxedValue = MergedItems.create(im, revealedItems)
|
||||
scrollView.updateItems(mergedItems.boxedValue.items)
|
||||
if im.itemAdded {
|
||||
im.itemAdded = false
|
||||
@@ -583,7 +638,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
.onChange(of: revealedItems) { revealed in
|
||||
mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealed, im.chatState)
|
||||
mergedItems.boxedValue = MergedItems.create(im, revealed)
|
||||
scrollView.updateItems(mergedItems.boxedValue.items)
|
||||
}
|
||||
.onChange(of: chat.id) { _ in
|
||||
@@ -618,7 +673,7 @@ struct ChatView: View {
|
||||
|
||||
private func updateWithInitiallyLoadedItems() {
|
||||
if mergedItems.boxedValue.items.isEmpty {
|
||||
mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, ItemsModel.shared.chatState)
|
||||
mergedItems.boxedValue = MergedItems.create(im, revealedItems)
|
||||
}
|
||||
let unreadIndex = mergedItems.boxedValue.items.lastIndex(where: { $0.hasUnread() })
|
||||
let unreadItemId: Int64? = if let unreadIndex { mergedItems.boxedValue.items[unreadIndex].newest().item.id } else { nil }
|
||||
@@ -638,8 +693,8 @@ struct ChatView: View {
|
||||
|
||||
private func searchTextChanged(_ s: String) {
|
||||
Task {
|
||||
await loadChat(chat: chat, search: s)
|
||||
mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState)
|
||||
await loadChat(chat: chat, im: im, search: s)
|
||||
mergedItems.boxedValue = MergedItems.create(im, revealedItems)
|
||||
await MainActor.run {
|
||||
scrollView.updateItems(mergedItems.boxedValue.items)
|
||||
}
|
||||
@@ -654,79 +709,8 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingButtonModel: ObservableObject {
|
||||
@Published var unreadAbove: Int = 0
|
||||
@Published var unreadBelow: Int = 0
|
||||
@Published var isNearBottom: Bool = true
|
||||
@Published var date: Date? = nil
|
||||
@Published var isDateVisible: Bool = false
|
||||
var hideDateWorkItem: DispatchWorkItem? = nil
|
||||
|
||||
func updateOnListChange(_ listState: EndlessScrollView<MergedItem>.ListState) {
|
||||
let lastVisibleItem = oldestPartiallyVisibleListItemInListStateOrNull(listState)
|
||||
let unreadBelow = if let lastVisibleItem {
|
||||
max(0, ItemsModel.shared.chatState.unreadTotal - lastVisibleItem.unreadBefore)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
let unreadAbove = ItemsModel.shared.chatState.unreadTotal - unreadBelow
|
||||
let date: Date? =
|
||||
if let lastVisible = listState.visibleItems.last {
|
||||
Calendar.current.startOfDay(for: lastVisible.item.oldest().item.meta.itemTs)
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
// set the counters and date indicator
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let it = self else { return }
|
||||
it.setDate(visibility: true)
|
||||
it.unreadAbove = unreadAbove
|
||||
it.unreadBelow = unreadBelow
|
||||
it.date = date
|
||||
}
|
||||
|
||||
// set floating button indication mode
|
||||
let nearBottom = listState.firstVisibleItemIndex < 1
|
||||
if nearBottom != self.isNearBottom {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in
|
||||
self?.isNearBottom = nearBottom
|
||||
}
|
||||
}
|
||||
|
||||
// hide Date indicator after 1 second of no scrolling
|
||||
hideDateWorkItem?.cancel()
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
guard let it = self else { return }
|
||||
it.setDate(visibility: false)
|
||||
it.hideDateWorkItem = nil
|
||||
}
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.hideDateWorkItem = workItem
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: workItem)
|
||||
}
|
||||
}
|
||||
|
||||
func resetDate() {
|
||||
date = nil
|
||||
isDateVisible = false
|
||||
}
|
||||
|
||||
private func setDate(visibility isVisible: Bool) {
|
||||
if isVisible {
|
||||
if !isNearBottom,
|
||||
!isDateVisible,
|
||||
let date, !Calendar.current.isDateInToday(date) {
|
||||
withAnimation { self.isDateVisible = true }
|
||||
}
|
||||
} else if isDateVisible {
|
||||
withAnimation { self.isDateVisible = false }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private struct FloatingButtons: View {
|
||||
@ObservedObject var im: ItemsModel
|
||||
let theme: AppTheme
|
||||
let scrollView: EndlessScrollView<MergedItem>
|
||||
let chat: Chat
|
||||
@@ -796,7 +780,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
if loadingBottomItems || !ItemsModel.shared.lastItemsLoaded {
|
||||
if loadingBottomItems || !im.lastItemsLoaded {
|
||||
requestedTopScroll = false
|
||||
requestedBottomScroll = true
|
||||
} else {
|
||||
@@ -816,7 +800,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
.onChange(of: loadingBottomItems) { loading in
|
||||
if !loading && requestedBottomScroll && ItemsModel.shared.lastItemsLoaded {
|
||||
if !loading && requestedBottomScroll && im.lastItemsLoaded {
|
||||
requestedBottomScroll = false
|
||||
scrollToBottom()
|
||||
}
|
||||
@@ -826,9 +810,9 @@ struct ChatView: View {
|
||||
|
||||
private func scrollToTopUnread() {
|
||||
Task {
|
||||
if !ItemsModel.shared.chatState.splits.isEmpty {
|
||||
if !im.chatState.splits.isEmpty {
|
||||
await MainActor.run { loadingMoreItems = true }
|
||||
await loadChat(chatId: chat.id, openAroundItemId: nil, clearItems: false)
|
||||
await loadChat(chatId: chat.id, im: im, openAroundItemId: nil, clearItems: false)
|
||||
await MainActor.run { reloadItems() }
|
||||
if let index = listState.items.lastIndex(where: { $0.hasUnread() }) {
|
||||
await MainActor.run { animatedScrollingInProgress = true }
|
||||
@@ -938,7 +922,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: {
|
||||
@@ -948,7 +932,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 link = try apiGetGroupLink(gInfo.groupId) {
|
||||
@@ -999,6 +983,7 @@ struct ChatView: View {
|
||||
let (validItems, confirmation) = try await apiPlanForwardChatItems(
|
||||
type: chat.chatInfo.chatType,
|
||||
id: chat.chatInfo.apiId,
|
||||
scope: chat.chatInfo.groupChatScope(),
|
||||
itemIds: Array(selectedChatItems)
|
||||
)
|
||||
if let confirmation {
|
||||
@@ -1088,7 +1073,6 @@ struct ChatView: View {
|
||||
}
|
||||
|
||||
func openForwardingSheet(_ items: [Int64]) async {
|
||||
let im = ItemsModel.shared
|
||||
var items = Set(items)
|
||||
var fci = [ChatItem]()
|
||||
for reversedChatItem in im.reversedChatItems {
|
||||
@@ -1127,11 +1111,11 @@ struct ChatView: View {
|
||||
private func loadChatItemsUnchecked(_ chat: Chat, _ pagination: ChatPagination) async -> Bool {
|
||||
await apiLoadMessages(
|
||||
chat.chatInfo.id,
|
||||
im,
|
||||
pagination,
|
||||
im.chatState,
|
||||
searchText,
|
||||
nil,
|
||||
{ visibleItemIndexesNonReversed(scrollView.listState, mergedItems.boxedValue) }
|
||||
{ visibleItemIndexesNonReversed(im, scrollView.listState, mergedItems.boxedValue) }
|
||||
)
|
||||
return true
|
||||
}
|
||||
@@ -1143,11 +1127,12 @@ struct ChatView: View {
|
||||
|
||||
func onChatItemsUpdated() {
|
||||
if !mergedItems.boxedValue.isActualState() {
|
||||
//logger.debug("Items are not actual, waiting for the next update: \(String(describing: mergedItems.boxedValue.splits)) \(ItemsModel.shared.chatState.splits), \(mergedItems.boxedValue.indexInParentItems.count) vs \(ItemsModel.shared.reversedChatItems.count)")
|
||||
//logger.debug("Items are not actual, waiting for the next update: \(String(describing: mergedItems.boxedValue.splits)) \(im.chatState.splits), \(mergedItems.boxedValue.indexInParentItems.count) vs \(im.reversedChatItems.count)")
|
||||
return
|
||||
}
|
||||
floatingButtonModel.updateOnListChange(scrollView.listState)
|
||||
preloadIfNeeded(
|
||||
im,
|
||||
$allowLoadMoreItems,
|
||||
$ignoreLoadingRequests,
|
||||
scrollView.listState,
|
||||
@@ -1161,13 +1146,14 @@ struct ChatView: View {
|
||||
},
|
||||
loadLastItems: {
|
||||
if !loadingMoreItems {
|
||||
await loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat)
|
||||
await loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat, im)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private struct ChatItemWithMenu: View {
|
||||
@ObservedObject var im: ItemsModel
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileRadius = defaultProfileImageCorner
|
||||
@@ -1252,8 +1238,6 @@ struct ChatView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let im = ItemsModel.shared
|
||||
|
||||
let last = isLastItem ? im.reversedChatItems.last : nil
|
||||
let listItem = merged.newest()
|
||||
let item = listItem.item
|
||||
@@ -1298,11 +1282,17 @@ struct ChatView: View {
|
||||
if !itemIds.isEmpty {
|
||||
waitToMarkRead {
|
||||
await apiMarkChatItemsRead(chat.chatInfo, itemIds, mentionsRead: unreadMentions)
|
||||
if (im.secondaryIMFilter != nil) {
|
||||
m.decreaseGroupSupportChatsUnreadCounter(chat.chatInfo.id, by: itemIds.count )
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if chatItem.isRcvNew {
|
||||
waitToMarkRead {
|
||||
await apiMarkChatItemsRead(chat.chatInfo, [chatItem.id], mentionsRead: chatItem.meta.userMention ? 1 : 0)
|
||||
if (im.secondaryIMFilter != nil) {
|
||||
m.decreaseGroupSupportChatsUnreadCounter(chat.chatInfo.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1324,7 +1314,6 @@ struct ChatView: View {
|
||||
}
|
||||
|
||||
private func unreadItemIds(_ range: ClosedRange<Int>) -> ([ChatItem.ID], Int) {
|
||||
let im = ItemsModel.shared
|
||||
var unreadItems: [ChatItem.ID] = []
|
||||
var unreadMentions: Int = 0
|
||||
|
||||
@@ -1537,6 +1526,7 @@ struct ChatView: View {
|
||||
}
|
||||
ChatItemView(
|
||||
chat: chat,
|
||||
im: im,
|
||||
chatItem: ci,
|
||||
scrollToItemId: scrollToItemId,
|
||||
maxWidth: maxWidth,
|
||||
@@ -1578,7 +1568,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.chatInfo, reports.sorted(), true)
|
||||
@@ -1627,7 +1617,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
switch chat.chatInfo {
|
||||
case let .group(groupInfo):
|
||||
case let .group(groupInfo, _):
|
||||
v.contextMenu {
|
||||
ReactionContextMenu(
|
||||
groupInfo: groupInfo,
|
||||
@@ -1650,7 +1640,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)
|
||||
}
|
||||
@@ -1709,7 +1699,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 {
|
||||
@@ -1820,6 +1810,7 @@ struct ChatView: View {
|
||||
let chatItem = try await apiChatItemReaction(
|
||||
type: cInfo.chatType,
|
||||
id: cInfo.apiId,
|
||||
scope: cInfo.groupChatScope(),
|
||||
itemId: ci.id,
|
||||
add: add,
|
||||
reaction: reaction
|
||||
@@ -1933,11 +1924,11 @@ struct ChatView: View {
|
||||
Task {
|
||||
do {
|
||||
let cInfo = chat.chatInfo
|
||||
let ciInfo = try await apiGetChatItemInfo(type: cInfo.chatType, id: cInfo.apiId, itemId: ci.id)
|
||||
let ciInfo = try await apiGetChatItemInfo(type: cInfo.chatType, id: cInfo.apiId, scope: cInfo.groupChatScope(), itemId: ci.id)
|
||||
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 {
|
||||
@@ -1991,13 +1982,13 @@ struct ChatView: View {
|
||||
private func deleteButton(_ ci: ChatItem, label: LocalizedStringKey = "Delete") -> Button<some View> {
|
||||
Button(role: .destructive) {
|
||||
if !revealed,
|
||||
let currIndex = m.getChatItemIndex(ci),
|
||||
let currIndex = m.getChatItemIndex(im, ci),
|
||||
let ciCategory = ci.mergeCategory {
|
||||
let (prevHidden, _) = m.getPrevShownChatItem(currIndex, ciCategory)
|
||||
if let range = itemsRange(currIndex, prevHidden) {
|
||||
var itemIds: [Int64] = []
|
||||
for i in range {
|
||||
itemIds.append(ItemsModel.shared.reversedChatItems[i].id)
|
||||
itemIds.append(im.reversedChatItems[i].id)
|
||||
}
|
||||
showDeleteMessages = true
|
||||
deletingItems = itemIds
|
||||
@@ -2135,12 +2126,12 @@ struct ChatView: View {
|
||||
selectedChatItems = selectedChatItems ?? []
|
||||
var itemIds: [Int64] = []
|
||||
if !revealed,
|
||||
let currIndex = m.getChatItemIndex(ci),
|
||||
let currIndex = m.getChatItemIndex(im, ci),
|
||||
let ciCategory = ci.mergeCategory {
|
||||
let (prevHidden, _) = m.getPrevShownChatItem(currIndex, ciCategory)
|
||||
if let range = itemsRange(currIndex, prevHidden) {
|
||||
for i in range {
|
||||
itemIds.append(ItemsModel.shared.reversedChatItems[i].id)
|
||||
itemIds.append(im.reversedChatItems[i].id)
|
||||
}
|
||||
} else {
|
||||
itemIds.append(ci.id)
|
||||
@@ -2174,6 +2165,7 @@ struct ChatView: View {
|
||||
try await apiDeleteChatItems(
|
||||
type: chat.chatInfo.chatType,
|
||||
id: chat.chatInfo.apiId,
|
||||
scope: chat.chatInfo.groupChatScope(),
|
||||
itemIds: [di.id],
|
||||
mode: mode
|
||||
)
|
||||
@@ -2231,14 +2223,14 @@ struct ChatView: View {
|
||||
if searchIsNotBlank {
|
||||
goToItemInnerButton(alignStart, "magnifyingglass", touchInProgress: touchInProgress) {
|
||||
closeKeyboardAndRun {
|
||||
ItemsModel.shared.loadOpenChatNoWait(chat.id, chatItem.id)
|
||||
im.loadOpenChatNoWait(chat.id, chatItem.id)
|
||||
}
|
||||
}
|
||||
} else if let chatTypeApiIdMsgId {
|
||||
goToItemInnerButton(alignStart, "arrow.right", touchInProgress: touchInProgress) {
|
||||
closeKeyboardAndRun {
|
||||
let (chatType, apiId, msgId) = chatTypeApiIdMsgId
|
||||
ItemsModel.shared.loadOpenChatNoWait("\(chatType.rawValue)\(apiId)", msgId)
|
||||
im.loadOpenChatNoWait("\(chatType.rawValue)\(apiId)", msgId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2265,6 +2257,84 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingButtonModel: ObservableObject {
|
||||
@ObservedObject var im: ItemsModel
|
||||
|
||||
public init(im: ItemsModel) {
|
||||
self.im = im
|
||||
}
|
||||
|
||||
@Published var unreadAbove: Int = 0
|
||||
@Published var unreadBelow: Int = 0
|
||||
@Published var isNearBottom: Bool = true
|
||||
@Published var date: Date? = nil
|
||||
@Published var isDateVisible: Bool = false
|
||||
var hideDateWorkItem: DispatchWorkItem? = nil
|
||||
|
||||
func updateOnListChange(_ listState: EndlessScrollView<MergedItem>.ListState) {
|
||||
let lastVisibleItem = oldestPartiallyVisibleListItemInListStateOrNull(listState)
|
||||
let unreadBelow = if let lastVisibleItem {
|
||||
max(0, im.chatState.unreadTotal - lastVisibleItem.unreadBefore)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
let unreadAbove = im.chatState.unreadTotal - unreadBelow
|
||||
let date: Date? =
|
||||
if let lastVisible = listState.visibleItems.last {
|
||||
Calendar.current.startOfDay(for: lastVisible.item.oldest().item.meta.itemTs)
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
// set the counters and date indicator
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let it = self else { return }
|
||||
it.setDate(visibility: true)
|
||||
it.unreadAbove = unreadAbove
|
||||
it.unreadBelow = unreadBelow
|
||||
it.date = date
|
||||
}
|
||||
|
||||
// set floating button indication mode
|
||||
let nearBottom = listState.firstVisibleItemIndex < 1
|
||||
if nearBottom != self.isNearBottom {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in
|
||||
self?.isNearBottom = nearBottom
|
||||
}
|
||||
}
|
||||
|
||||
// hide Date indicator after 1 second of no scrolling
|
||||
hideDateWorkItem?.cancel()
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
guard let it = self else { return }
|
||||
it.setDate(visibility: false)
|
||||
it.hideDateWorkItem = nil
|
||||
}
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.hideDateWorkItem = workItem
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: workItem)
|
||||
}
|
||||
}
|
||||
|
||||
func resetDate() {
|
||||
date = nil
|
||||
isDateVisible = false
|
||||
}
|
||||
|
||||
private func setDate(visibility isVisible: Bool) {
|
||||
if isVisible {
|
||||
if !isNearBottom,
|
||||
!isDateVisible,
|
||||
let date, !Calendar.current.isDateInToday(date) {
|
||||
withAnimation { self.isDateVisible = true }
|
||||
}
|
||||
} else if isDateVisible {
|
||||
withAnimation { self.isDateVisible = false }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func broadcastDeleteButtonText(_ chat: Chat) -> LocalizedStringKey {
|
||||
chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone"
|
||||
}
|
||||
@@ -2286,6 +2356,7 @@ private func deleteMessages(_ chat: Chat, _ deletingItems: [Int64], _ mode: CIDe
|
||||
try await apiDeleteChatItems(
|
||||
type: chatInfo.chatType,
|
||||
id: chatInfo.apiId,
|
||||
scope: chatInfo.groupChatScope(),
|
||||
itemIds: itemIds,
|
||||
mode: mode
|
||||
)
|
||||
@@ -2347,7 +2418,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?
|
||||
@@ -2500,7 +2571,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: ()
|
||||
@@ -2517,7 +2588,8 @@ struct ChatView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
chatModel.chatId = "@1"
|
||||
ItemsModel.shared.reversedChatItems = [
|
||||
let im = ItemsModel.shared
|
||||
im.reversedChatItems = [
|
||||
ChatItem.getSample(1, .directSnd, .now, "hello"),
|
||||
ChatItem.getSample(2, .directRcv, .now, "hi"),
|
||||
ChatItem.getSample(3, .directRcv, .now, "hi there"),
|
||||
@@ -2529,7 +2601,12 @@ struct ChatView_Previews: PreviewProvider {
|
||||
ChatItem.getSample(9, .directSnd, .now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
|
||||
]
|
||||
@State var showChatInfo = false
|
||||
return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []))
|
||||
.environmentObject(chatModel)
|
||||
return ChatView(
|
||||
chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []),
|
||||
im: im,
|
||||
mergedItems: BoxedValue(MergedItems.create(im, [])),
|
||||
floatingButtonModel: FloatingButtonModel(im: im)
|
||||
)
|
||||
.environmentObject(chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,6 +323,7 @@ struct ComposeView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
@ObservedObject var im: ItemsModel
|
||||
@Binding var composeState: ComposeState
|
||||
@Binding var keyboardVisible: Bool
|
||||
@Binding var keyboardHiddenDate: Date
|
||||
@@ -355,6 +356,20 @@ struct ComposeView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
if let groupInfo = chat.chatInfo.groupInfo,
|
||||
case let .groupChatScopeContext(groupScopeInfo) = im.secondaryIMFilter,
|
||||
case let .memberSupport(member) = groupScopeInfo,
|
||||
let member = member,
|
||||
member.memberPending,
|
||||
composeState.contextItem == .noContextItem,
|
||||
composeState.noPreview {
|
||||
ContextPendingMemberActionsView(
|
||||
groupInfo: groupInfo,
|
||||
member: member
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
|
||||
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
|
||||
ContextInvitingContactMemberView()
|
||||
Divider()
|
||||
@@ -396,7 +411,7 @@ struct ComposeView: View {
|
||||
.padding(.bottom, 16)
|
||||
.padding(.leading, 12)
|
||||
.tint(theme.colors.primary)
|
||||
if case let .group(g) = chat.chatInfo,
|
||||
if case let .group(g, _) = chat.chatInfo,
|
||||
!g.fullGroupPreferences.files.on(for: g.membership) {
|
||||
b.disabled(true).onTapGesture {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -443,17 +458,30 @@ struct ComposeView: View {
|
||||
.padding(.trailing, 12)
|
||||
.disabled(!chat.userCanSend)
|
||||
|
||||
if chat.userIsObserver {
|
||||
Text("you are observer")
|
||||
.italic()
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "You can't send messages!",
|
||||
message: "Please contact group admin."
|
||||
)
|
||||
}
|
||||
if im.secondaryIMFilter == nil {
|
||||
if chat.userIsPending {
|
||||
Text("reviewed by moderators")
|
||||
.italic()
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "You can't send messages!",
|
||||
message: "Please contact group admin."
|
||||
)
|
||||
}
|
||||
} else if chat.userIsObserver {
|
||||
Text("you are observer")
|
||||
.italic()
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "You can't send messages!",
|
||||
message: "Please contact group admin."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -944,6 +972,7 @@ struct ComposeView: View {
|
||||
let chatItem = try await apiUpdateChatItem(
|
||||
type: chat.chatInfo.chatType,
|
||||
id: chat.chatInfo.apiId,
|
||||
scope: chat.chatInfo.groupChatScope(),
|
||||
itemId: ei.id,
|
||||
updatedMessage: UpdatedMessage(msgContent: mc, mentions: composeState.memberMentions),
|
||||
live: live
|
||||
@@ -1031,6 +1060,7 @@ struct ComposeView: View {
|
||||
: await apiSendMessages(
|
||||
type: chat.chatInfo.chatType,
|
||||
id: chat.chatInfo.apiId,
|
||||
scope: chat.chatInfo.groupChatScope(),
|
||||
live: live,
|
||||
ttl: ttl,
|
||||
composedMessages: msgs
|
||||
@@ -1055,8 +1085,10 @@ struct ComposeView: View {
|
||||
if let chatItems = await apiForwardChatItems(
|
||||
toChatType: chat.chatInfo.chatType,
|
||||
toChatId: chat.chatInfo.apiId,
|
||||
toScope: chat.chatInfo.groupChatScope(),
|
||||
fromChatType: fromChatInfo.chatType,
|
||||
fromChatId: fromChatInfo.apiId,
|
||||
fromScope: fromChatInfo.groupChatScope(),
|
||||
itemIds: forwardedItems.map { $0.id },
|
||||
ttl: ttl
|
||||
) {
|
||||
@@ -1274,12 +1306,14 @@ struct ComposeView: View {
|
||||
struct ComposeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
||||
let im = ItemsModel.shared
|
||||
@State var composeState = ComposeState(message: "hello")
|
||||
@State var selectedRange = NSRange()
|
||||
|
||||
return Group {
|
||||
ComposeView(
|
||||
chat: chat,
|
||||
im: im,
|
||||
composeState: $composeState,
|
||||
keyboardVisible: Binding.constant(true),
|
||||
keyboardHiddenDate: Binding.constant(Date.now),
|
||||
@@ -1288,6 +1322,7 @@ struct ComposeView_Previews: PreviewProvider {
|
||||
.environmentObject(ChatModel())
|
||||
ComposeView(
|
||||
chat: chat,
|
||||
im: im,
|
||||
composeState: $composeState,
|
||||
keyboardVisible: Binding.constant(true),
|
||||
keyboardHiddenDate: Binding.constant(Date.now),
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// ContextPendingMemberActionsView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 02.05.2025.
|
||||
// Copyright © 2025 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
// TODO [knocking] go back (close secondary ChatView) on actions
|
||||
struct ContextPendingMemberActionsView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var groupInfo: GroupInfo
|
||||
var member: GroupMember
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
ZStack {
|
||||
Text("Remove")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
showRemoveMemberAlert(groupInfo, member)
|
||||
}
|
||||
|
||||
ZStack {
|
||||
Text("Accept")
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
showAcceptMemberAlert(groupInfo, member)
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 54)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(.thinMaterial)
|
||||
}
|
||||
}
|
||||
|
||||
func showAcceptMemberAlert(_ groupInfo: GroupInfo, _ member: GroupMember) {
|
||||
showAlert(
|
||||
NSLocalizedString("Accept member", comment: "alert title"),
|
||||
message: NSLocalizedString("Member will join the group, accept member?", comment: "alert message"),
|
||||
actions: {[
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Accept as member", comment: "alert action"),
|
||||
style: .default,
|
||||
handler: { _ in
|
||||
acceptMember(groupInfo, member, .member)
|
||||
}
|
||||
),
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Accept as observer", comment: "alert action"),
|
||||
style: .default,
|
||||
handler: { _ in
|
||||
acceptMember(groupInfo, member, .observer)
|
||||
}
|
||||
),
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Cancel", comment: "alert action"),
|
||||
style: .default
|
||||
)
|
||||
]}
|
||||
)
|
||||
}
|
||||
|
||||
func acceptMember(_ groupInfo: GroupInfo, _ member: GroupMember, _ role: GroupMemberRole) {
|
||||
Task {
|
||||
do {
|
||||
let acceptedMember = try await apiAcceptMember(groupInfo.groupId, member.groupMemberId, role)
|
||||
await MainActor.run {
|
||||
_ = ChatModel.shared.upsertGroupMember(groupInfo, acceptedMember)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiAcceptMember error: \(responseError(error))")
|
||||
await MainActor.run {
|
||||
showAlert(
|
||||
NSLocalizedString("Error accepting member", comment: "alert title"),
|
||||
message: responseError(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContextPendingMemberActionsView(
|
||||
groupInfo: GroupInfo.sampleData,
|
||||
member: GroupMember.sampleData
|
||||
)
|
||||
}
|
||||
@@ -87,7 +87,26 @@ struct GroupChatInfoView: View {
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
|
||||
|
||||
Section {
|
||||
if groupInfo.membership.supportChat != nil {
|
||||
let scopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: nil)
|
||||
UserSupportChatNavLink(
|
||||
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo), chatItems: [], chatStats: ChatStats()),
|
||||
im: ItemsModel(secondaryIMFilter: .groupChatScopeContext(groupScopeInfo: scopeInfo))
|
||||
)
|
||||
}
|
||||
if groupInfo.businessChat == nil && groupInfo.membership.memberRole >= .moderator {
|
||||
memberSupportButton()
|
||||
}
|
||||
if groupInfo.canModerate {
|
||||
GroupReportsChatNavLink(
|
||||
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: nil), chatItems: [], chatStats: ChatStats()),
|
||||
im: ItemsModel(secondaryIMFilter: .msgContentTagContext(contentTag: .report))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
if groupInfo.isOwner && groupInfo.businessChat == nil {
|
||||
editGroupButton()
|
||||
@@ -96,17 +115,6 @@ struct GroupChatInfoView: View {
|
||||
addOrEditWelcomeMessage()
|
||||
}
|
||||
GroupPreferencesButton(groupInfo: $groupInfo, preferences: groupInfo.fullGroupPreferences, currentPreferences: groupInfo.fullGroupPreferences)
|
||||
if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT {
|
||||
sendReceiptsOption()
|
||||
} else {
|
||||
sendReceiptsOptionDisabled()
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
ChatWallpaperEditorSheet(chat: chat)
|
||||
} label: {
|
||||
Label("Chat theme", systemImage: "photo")
|
||||
}
|
||||
} header: {
|
||||
Text("")
|
||||
} footer: {
|
||||
@@ -120,6 +128,16 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
|
||||
Section {
|
||||
if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT {
|
||||
sendReceiptsOption()
|
||||
} else {
|
||||
sendReceiptsOptionDisabled()
|
||||
}
|
||||
NavigationLink {
|
||||
ChatWallpaperEditorSheet(chat: chat)
|
||||
} label: {
|
||||
Label("Chat theme", systemImage: "photo")
|
||||
}
|
||||
ChatTTLOption(chat: chat, progressIndicator: $progressIndicator)
|
||||
} footer: {
|
||||
Text("Delete chat messages from your device.")
|
||||
@@ -519,6 +537,81 @@ struct GroupChatInfoView: View {
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
|
||||
struct UserSupportChatNavLink: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State private var userSupportChatNavLinkActive = false
|
||||
@ObservedObject var chat: Chat
|
||||
var im: ItemsModel
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Button {
|
||||
im.loadOpenChat(chat.id) {
|
||||
userSupportChatNavLinkActive = true
|
||||
}
|
||||
} label: {
|
||||
Label("Support chat", systemImage: "flag")
|
||||
}
|
||||
|
||||
NavigationLink(isActive: $userSupportChatNavLinkActive) {
|
||||
if let secondaryIM = chatModel.secondaryIM {
|
||||
SecondaryChatView(
|
||||
chat: chat,
|
||||
im: secondaryIM
|
||||
)
|
||||
}
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.frame(width: 1, height: 1)
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func memberSupportButton() -> some View {
|
||||
NavigationLink {
|
||||
MemberSupportView(groupInfo: groupInfo)
|
||||
.navigationBarTitle("Member support")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
Label("Member support", systemImage: "flag")
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupReportsChatNavLink: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State private var groupReportsChatNavLinkActive = false
|
||||
@ObservedObject var chat: Chat
|
||||
var im: ItemsModel
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Button {
|
||||
im.loadOpenChat(chat.id) {
|
||||
groupReportsChatNavLinkActive = true
|
||||
}
|
||||
} label: {
|
||||
Label("Member reports", systemImage: "flag")
|
||||
}
|
||||
|
||||
NavigationLink(isActive: $groupReportsChatNavLinkActive) {
|
||||
if let secondaryIM = chatModel.secondaryIM {
|
||||
SecondaryChatView(
|
||||
chat: chat,
|
||||
im: secondaryIM
|
||||
)
|
||||
}
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.frame(width: 1, height: 1)
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func editGroupButton() -> some View {
|
||||
NavigationLink {
|
||||
GroupProfileView(
|
||||
@@ -679,26 +772,34 @@ struct GroupChatInfoView: View {
|
||||
title: Text("Remove member?"),
|
||||
message: Text(messageLabel),
|
||||
primaryButton: .destructive(Text("Remove")) {
|
||||
Task {
|
||||
do {
|
||||
let updatedMembers = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId])
|
||||
await MainActor.run {
|
||||
updatedMembers.forEach { updatedMember in
|
||||
_ = chatModel.upsertGroupMember(groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiRemoveMembers error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error removing member")
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
removeMember(groupInfo, mem)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func removeMember(_ groupInfo: GroupInfo, _ mem: GroupMember) {
|
||||
Task {
|
||||
do {
|
||||
let updatedMembers = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId])
|
||||
await MainActor.run {
|
||||
updatedMembers.forEach { updatedMember in
|
||||
_ = ChatModel.shared.upsertGroupMember(groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiRemoveMembers error: \(responseError(error))")
|
||||
await MainActor.run {
|
||||
showAlert(
|
||||
NSLocalizedString("Error removing member", comment: "alert title"),
|
||||
message: responseError(error)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteGroupAlertMessage(_ groupInfo: GroupInfo) -> Text {
|
||||
groupInfo.businessChat == nil ? (
|
||||
groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!")
|
||||
|
||||
@@ -278,7 +278,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,14 @@ struct GroupPreferencesView: View {
|
||||
let saveText: LocalizedStringKey = creatingGroup ? "Save" : "Save and notify group members"
|
||||
VStack {
|
||||
List {
|
||||
Section {
|
||||
MemberAdmissionButton(
|
||||
groupInfo: $groupInfo,
|
||||
admission: groupInfo.groupProfile.memberAdmission ?? GroupMemberAdmission(),
|
||||
currentAdmission: groupInfo.groupProfile.memberAdmission ?? GroupMemberAdmission(),
|
||||
creatingGroup: creatingGroup
|
||||
)
|
||||
}
|
||||
featureSection(.timedMessages, $preferences.timedMessages.enable)
|
||||
featureSection(.fullDelete, $preferences.fullDelete.enable)
|
||||
featureSection(.directMessages, $preferences.directMessages.enable, $preferences.directMessages.role)
|
||||
@@ -77,6 +85,62 @@ struct GroupPreferencesView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct MemberAdmissionButton: View {
|
||||
@Binding var groupInfo: GroupInfo
|
||||
@State var admission: GroupMemberAdmission
|
||||
@State var currentAdmission: GroupMemberAdmission
|
||||
var creatingGroup: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationLink {
|
||||
MemberAdmissionView(
|
||||
groupInfo: $groupInfo,
|
||||
admission: $admission,
|
||||
currentAdmission: currentAdmission,
|
||||
creatingGroup: creatingGroup,
|
||||
saveAdmission: saveAdmission
|
||||
)
|
||||
.navigationBarTitle("Member admission")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.onDisappear {
|
||||
let saveText = NSLocalizedString(
|
||||
creatingGroup ? "Save" : "Save and notify group members",
|
||||
comment: "alert button"
|
||||
)
|
||||
|
||||
if groupInfo.groupProfile.memberAdmission != admission {
|
||||
showAlert(
|
||||
title: NSLocalizedString("Save admission settings?", comment: "alert title"),
|
||||
buttonTitle: saveText,
|
||||
buttonAction: { saveAdmission() },
|
||||
cancelButton: true
|
||||
)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Member admission", systemImage: "switch.2")
|
||||
}
|
||||
}
|
||||
|
||||
private func saveAdmission() {
|
||||
Task {
|
||||
do {
|
||||
var gp = groupInfo.groupProfile
|
||||
gp.memberAdmission = admission
|
||||
let gInfo = try await apiUpdateGroup(groupInfo.groupId, gp)
|
||||
await MainActor.run {
|
||||
groupInfo = gInfo
|
||||
ChatModel.shared.updateGroup(gInfo)
|
||||
currentAdmission = admission
|
||||
}
|
||||
} catch {
|
||||
logger.error("MemberAdmissionView apiUpdateGroup error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding<GroupFeatureEnabled>, _ enableForRole: Binding<GroupMemberRole?>? = nil) -> some View {
|
||||
Section {
|
||||
let color: Color = enableFeature.wrappedValue == .on ? .green : theme.colors.secondary
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// MemberAdmissionView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 28.04.2025.
|
||||
// Copyright © 2025 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
private let memberCriterias: [(criteria: MemberCriteria?, text: LocalizedStringKey)] = [
|
||||
(nil, "off"),
|
||||
(.all, "all")
|
||||
]
|
||||
|
||||
struct MemberAdmissionView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Binding var groupInfo: GroupInfo
|
||||
@Binding var admission: GroupMemberAdmission
|
||||
var currentAdmission: GroupMemberAdmission
|
||||
let creatingGroup: Bool
|
||||
let saveAdmission: () -> Void
|
||||
@State private var showSaveDialogue = false
|
||||
|
||||
var body: some View {
|
||||
let saveText: LocalizedStringKey = creatingGroup ? "Save" : "Save and notify group members"
|
||||
VStack {
|
||||
List {
|
||||
admissionSection(
|
||||
NSLocalizedString("Review", comment: "admission stage"),
|
||||
NSLocalizedString("Review new members before admitting to group.", comment: "admission stage description"),
|
||||
$admission.review
|
||||
)
|
||||
|
||||
if groupInfo.isOwner {
|
||||
Section {
|
||||
Button("Reset") { admission = currentAdmission }
|
||||
Button(saveText) { saveAdmission() }
|
||||
}
|
||||
.disabled(currentAdmission == admission)
|
||||
}
|
||||
}
|
||||
}
|
||||
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||
if currentAdmission == admission {
|
||||
dismiss()
|
||||
} else {
|
||||
showSaveDialogue = true
|
||||
}
|
||||
})
|
||||
.confirmationDialog("Save admission settings?", isPresented: $showSaveDialogue) {
|
||||
Button(saveText) {
|
||||
saveAdmission()
|
||||
dismiss()
|
||||
}
|
||||
Button("Exit without saving") {
|
||||
admission = currentAdmission
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func admissionSection(_ admissionStageStr: String, _ admissionStageDescrStr: String, _ memberCriteria: Binding<MemberCriteria?>) -> some View {
|
||||
Section {
|
||||
if groupInfo.isOwner {
|
||||
Picker(admissionStageStr, selection: memberCriteria) {
|
||||
ForEach(memberCriterias, id: \.criteria) { mc in
|
||||
Text(mc.text)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
infoRow(Text(admissionStageStr), memberCriteria.wrappedValue?.text ?? NSLocalizedString("off", comment: "member criteria value"))
|
||||
}
|
||||
} footer: {
|
||||
Text(admissionStageDescrStr)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MemberAdmissionView(
|
||||
groupInfo: Binding.constant(GroupInfo.sampleData),
|
||||
admission: Binding.constant(GroupMemberAdmission.sampleData),
|
||||
currentAdmission: GroupMemberAdmission.sampleData,
|
||||
creatingGroup: false,
|
||||
saveAdmission: {}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// MemberSupportChatToolbar.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 01.05.2025.
|
||||
// Copyright © 2025 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct MemberSupportChatToolbar: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var groupMember: GroupMember
|
||||
var imageSize: CGFloat = 32
|
||||
|
||||
var body: some View {
|
||||
return HStack {
|
||||
MemberProfileImage(groupMember, size: imageSize)
|
||||
.padding(.trailing, 4)
|
||||
let t = Text(groupMember.displayName).font(.headline)
|
||||
(groupMember.verified ? memberVerifiedShield + t : t)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.foregroundColor(theme.colors.onBackground)
|
||||
.frame(width: 220)
|
||||
}
|
||||
|
||||
private var memberVerifiedShield: Text {
|
||||
(Text(Image(systemName: "checkmark.shield")) + textSpace)
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.baselineOffset(1)
|
||||
.kerning(-2)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MemberSupportChatToolbar(
|
||||
groupMember: GroupMember.sampleData
|
||||
)
|
||||
.environmentObject(CurrentColors.toAppTheme())
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
//
|
||||
// MemberSupportView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 28.04.2025.
|
||||
// Copyright © 2025 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct MemberSupportView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@State private var searchText: String = ""
|
||||
@FocusState private var searchFocussed
|
||||
var groupInfo: GroupInfo
|
||||
|
||||
var body: some View {
|
||||
viewBody()
|
||||
.onAppear {
|
||||
Task {
|
||||
await chatModel.loadGroupMembers(groupInfo)
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
Task {
|
||||
await chatModel.loadGroupMembers(groupInfo)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func viewBody() -> some View {
|
||||
let membersWithChats = sortedMembersWithChats()
|
||||
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
|
||||
let filteredMembersWithChats = s == ""
|
||||
? membersWithChats
|
||||
: membersWithChats.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) }
|
||||
|
||||
if membersWithChats.isEmpty {
|
||||
Text("No support chats")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
List {
|
||||
searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.onBackground, theme.colors.secondary)
|
||||
.padding(.leading, 8)
|
||||
ForEach(filteredMembersWithChats) { memberWithChat in
|
||||
let scopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: memberWithChat.wrapped)
|
||||
MemberSupportChatNavLink(
|
||||
groupInfo: groupInfo,
|
||||
memberWithChat: memberWithChat,
|
||||
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo), chatItems: [], chatStats: ChatStats()),
|
||||
im: ItemsModel(secondaryIMFilter: .groupChatScopeContext(groupScopeInfo: scopeInfo))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MemberSupportChatNavLink: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@State private var memberSupportChatNavLinkActive = false
|
||||
var groupInfo: GroupInfo
|
||||
var memberWithChat: GMember
|
||||
@ObservedObject var chat: Chat
|
||||
var im: ItemsModel
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Button {
|
||||
im.loadOpenChat(chat.id) {
|
||||
memberSupportChatNavLinkActive = true
|
||||
}
|
||||
} label: {
|
||||
SupportChatRowView(groupMember: memberWithChat, groupInfo: groupInfo)
|
||||
}
|
||||
|
||||
NavigationLink(isActive: $memberSupportChatNavLinkActive) {
|
||||
if let secondaryIM = chatModel.secondaryIM {
|
||||
SecondaryChatView(
|
||||
chat: chat,
|
||||
im: secondaryIM
|
||||
)
|
||||
}
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.frame(width: 1, height: 1)
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sortedMembersWithChats() -> [GMember] {
|
||||
chatModel.groupMembers
|
||||
.filter {
|
||||
$0.wrapped.supportChat != nil &&
|
||||
$0.wrapped.memberStatus != .memLeft &&
|
||||
$0.wrapped.memberStatus != .memRemoved
|
||||
}
|
||||
.sorted { (m0: GMember, m1: GMember) -> Bool in
|
||||
if m0.wrapped.memberPending != m1.wrapped.memberPending {
|
||||
return m0.wrapped.memberPending
|
||||
}
|
||||
|
||||
let mentions0 = (m0.wrapped.supportChat?.mentions ?? 0) > 0
|
||||
let mentions1 = (m1.wrapped.supportChat?.mentions ?? 0) > 0
|
||||
if mentions0 != mentions1 {
|
||||
return mentions0
|
||||
}
|
||||
|
||||
let attention0 = (m0.wrapped.supportChat?.memberAttention ?? 0) > 0
|
||||
let attention1 = (m1.wrapped.supportChat?.memberAttention ?? 0) > 0
|
||||
if attention0 != attention1 {
|
||||
return attention0
|
||||
}
|
||||
|
||||
let unread0 = (m0.wrapped.supportChat?.unread ?? 0) > 0
|
||||
let unread1 = (m1.wrapped.supportChat?.unread ?? 0) > 0
|
||||
if unread0 != unread1 {
|
||||
return unread0
|
||||
}
|
||||
|
||||
return (m0.wrapped.supportChat?.chatTs ?? .distantPast) > (m1.wrapped.supportChat?.chatTs ?? .distantPast)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SupportChatRowView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ObservedObject var groupMember: GMember
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
|
||||
var groupInfo: GroupInfo
|
||||
|
||||
var dynamicChatInfoSize: CGFloat { dynamicSize(userFont).chatInfoSize }
|
||||
|
||||
var body: some View {
|
||||
let member = groupMember.wrapped
|
||||
HStack{
|
||||
MemberProfileImage(member, size: 38)
|
||||
.padding(.trailing, 2)
|
||||
VStack(alignment: .leading) {
|
||||
let t = Text(member.chatViewName).foregroundColor(theme.colors.onBackground)
|
||||
(member.verified ? memberVerifiedShield + t : t)
|
||||
.lineLimit(1)
|
||||
Text(memberStatus(member))
|
||||
.lineLimit(1)
|
||||
.font(.caption)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if member.memberPending {
|
||||
Image(systemName: "flag.fill")
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: dynamicChatInfoSize * 0.8, height: dynamicChatInfoSize * 0.8)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
if let supportChat = member.supportChat {
|
||||
SupportChatUnreadIndicator(supportChat: supportChat)
|
||||
}
|
||||
}
|
||||
// TODO [knocking] swipe actions are broken
|
||||
// .swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
// if member.memberPending {
|
||||
// Button {
|
||||
// showAcceptMemberAlert(groupInfo, member)
|
||||
// } label: {
|
||||
// Label("Accept", systemImage: "checkmark")
|
||||
// }
|
||||
// .tint(theme.colors.primary)
|
||||
// }
|
||||
//
|
||||
// Button {
|
||||
// showRemoveMemberAlert(groupInfo, member)
|
||||
// } label: {
|
||||
// Label("Remove", systemImage: "trash")
|
||||
// }
|
||||
// .tint(.red)
|
||||
// }
|
||||
}
|
||||
|
||||
private func memberStatus(_ member: GroupMember) -> LocalizedStringKey {
|
||||
if member.activeConn?.connDisabled ?? false {
|
||||
return "disabled"
|
||||
} else if member.activeConn?.connInactive ?? false {
|
||||
return "inactive"
|
||||
} else if member.memberPending {
|
||||
return member.memberStatus.text
|
||||
} else {
|
||||
return LocalizedStringKey(member.memberRole.text)
|
||||
}
|
||||
}
|
||||
|
||||
struct SupportChatUnreadIndicator: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
|
||||
var supportChat: GroupSupportChat
|
||||
|
||||
var dynamicChatInfoSize: CGFloat { dynamicSize(userFont).chatInfoSize }
|
||||
|
||||
private var indicatorTint: Color {
|
||||
if supportChat.mentions > 0 || supportChat.memberAttention > 0 {
|
||||
return theme.colors.primary
|
||||
} else {
|
||||
return theme.colors.secondary
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 2) {
|
||||
if supportChat.unread > 0 || supportChat.mentions > 0 || supportChat.memberAttention > 0 {
|
||||
if supportChat.mentions > 0 && supportChat.unread > 1 {
|
||||
Text("\(MENTION_START)")
|
||||
.font(userFont <= .xxxLarge ? .body : .callout)
|
||||
.foregroundColor(indicatorTint)
|
||||
.frame(minWidth: dynamicChatInfoSize, minHeight: dynamicChatInfoSize)
|
||||
.cornerRadius(dynamicSize(userFont).unreadCorner)
|
||||
.padding(.bottom, 1)
|
||||
}
|
||||
let singleUnreadIsMention = supportChat.mentions > 0 && supportChat.unread == 1
|
||||
(singleUnreadIsMention ? Text("\(MENTION_START)") : unreadCountText(supportChat.unread))
|
||||
.font(userFont <= .xxxLarge ? .caption : .caption2)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, dynamicSize(userFont).unreadPadding)
|
||||
.frame(minWidth: dynamicChatInfoSize, minHeight: dynamicChatInfoSize)
|
||||
.background(indicatorTint)
|
||||
.cornerRadius(dynamicSize(userFont).unreadCorner)
|
||||
}
|
||||
}
|
||||
.frame(height: dynamicChatInfoSize)
|
||||
.frame(minWidth: 22)
|
||||
}
|
||||
}
|
||||
|
||||
private var memberVerifiedShield: Text {
|
||||
(Text(Image(systemName: "checkmark.shield")) + textSpace)
|
||||
.font(.caption)
|
||||
.baselineOffset(2)
|
||||
.kerning(-2)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func showRemoveMemberAlert(_ groupInfo: GroupInfo, _ member: GroupMember) {
|
||||
showAlert(
|
||||
title: NSLocalizedString("Remove member?", comment: "alert title"),
|
||||
buttonTitle: "Remove",
|
||||
buttonAction: { removeMember(groupInfo, member) },
|
||||
cancelButton: true
|
||||
)
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MemberSupportView(
|
||||
groupInfo: GroupInfo.sampleData
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// SecondaryChatView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 29.04.2025.
|
||||
// Copyright © 2025 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct SecondaryChatView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ObservedObject var chat: Chat
|
||||
@ObservedObject var im: ItemsModel
|
||||
|
||||
var body: some View {
|
||||
ChatView(
|
||||
chat: chat,
|
||||
im: im,
|
||||
mergedItems: BoxedValue(MergedItems.create(im, [])),
|
||||
floatingButtonModel: FloatingButtonModel(im: im)
|
||||
)
|
||||
.onDisappear {
|
||||
chatModel.secondaryIM = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SecondaryChatView(
|
||||
chat: Chat(
|
||||
chatInfo: .group(groupInfo: GroupInfo.sampleData, groupChatScope: .memberSupport(groupMember_: GroupMember.sampleData)),
|
||||
chatItems: [],
|
||||
chatStats: ChatStats()
|
||||
),
|
||||
im: ItemsModel.shared
|
||||
)
|
||||
}
|
||||
@@ -25,7 +25,7 @@ struct SelectedItemsTopToolbar: View {
|
||||
struct SelectedItemsBottomToolbar: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let chatItems: [ChatItem]
|
||||
let im: ItemsModel
|
||||
@Binding var selectedChatItems: Set<Int64>?
|
||||
var chatInfo: ChatInfo
|
||||
// Bool - delete for everyone is possible
|
||||
@@ -75,9 +75,9 @@ struct SelectedItemsBottomToolbar: View {
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20, alignment: .center)
|
||||
.foregroundColor(!moderateEnabled || deleteCountProhibited ? theme.colors.secondary : .red)
|
||||
.foregroundColor(!moderateEnabled || deleteCountProhibited || im.secondaryIMFilter != nil ? theme.colors.secondary : .red)
|
||||
}
|
||||
.disabled(!moderateEnabled || deleteCountProhibited)
|
||||
.disabled(!moderateEnabled || deleteCountProhibited || im.secondaryIMFilter != nil)
|
||||
.opacity(canModerate ? 1 : 0)
|
||||
|
||||
Spacer()
|
||||
@@ -88,24 +88,24 @@ struct SelectedItemsBottomToolbar: View {
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20, alignment: .center)
|
||||
.foregroundColor(!forwardEnabled || forwardCountProhibited ? theme.colors.secondary : theme.colors.primary)
|
||||
.foregroundColor(!forwardEnabled || forwardCountProhibited || im.secondaryIMFilter != nil ? theme.colors.secondary : theme.colors.primary)
|
||||
}
|
||||
.disabled(!forwardEnabled || forwardCountProhibited)
|
||||
.disabled(!forwardEnabled || forwardCountProhibited || im.secondaryIMFilter != nil)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding([.leading, .trailing], 12)
|
||||
}
|
||||
.onAppear {
|
||||
recheckItems(chatInfo, chatItems, selectedChatItems)
|
||||
recheckItems(chatInfo, im.reversedChatItems, selectedChatItems)
|
||||
}
|
||||
.onChange(of: chatInfo) { info in
|
||||
recheckItems(info, chatItems, selectedChatItems)
|
||||
recheckItems(info, im.reversedChatItems, selectedChatItems)
|
||||
}
|
||||
.onChange(of: chatItems) { items in
|
||||
.onChange(of: im.reversedChatItems) { items in
|
||||
recheckItems(chatInfo, items, selectedChatItems)
|
||||
}
|
||||
.onChange(of: selectedChatItems) { selected in
|
||||
recheckItems(chatInfo, chatItems, selected)
|
||||
recheckItems(chatInfo, im.reversedChatItems, selected)
|
||||
}
|
||||
.frame(height: 55.5)
|
||||
.background(.thinMaterial)
|
||||
@@ -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 >= .admin
|
||||
default: false
|
||||
}
|
||||
|
||||
@@ -66,7 +66,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)
|
||||
|
||||
@@ -452,7 +452,13 @@ struct ChatListView: View {
|
||||
|
||||
@ViewBuilder private func chatView() -> some View {
|
||||
if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) {
|
||||
ChatView(chat: chat)
|
||||
let im = ItemsModel.shared
|
||||
ChatView(
|
||||
chat: chat,
|
||||
im: im,
|
||||
mergedItems: BoxedValue(MergedItems.create(im, [])),
|
||||
floatingButtonModel: FloatingButtonModel(im: im)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -898,12 +904,12 @@ func presetTagMatchesChat(_ tag: PresetTag, _ chatInfo: ChatInfo, _ chatStats: C
|
||||
case let .direct(contact): !(contact.activeConn == nil && contact.profile.contactLink != nil && contact.active) && !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
|
||||
case let .group(groupInfo, _): groupInfo.businessChat == nil
|
||||
default: false
|
||||
}
|
||||
case .business:
|
||||
|
||||
@@ -141,7 +141,7 @@ struct ChatPreviewView: View {
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
case let .group(groupInfo):
|
||||
case let .group(groupInfo, _):
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
case .memRejected: inactiveIcon()
|
||||
case .memLeft: inactiveIcon()
|
||||
@@ -165,7 +165,7 @@ struct ChatPreviewView: View {
|
||||
switch chat.chatInfo {
|
||||
case let .direct(contact):
|
||||
previewTitle(contact.verified == true ? verifiedIcon + t : t).foregroundColor(deleting ? Color.secondary : nil)
|
||||
case let .group(groupInfo):
|
||||
case let .group(groupInfo, _):
|
||||
let v = previewTitle(t)
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
case .memInvited: v.foregroundColor(deleting ? theme.colors.secondary : chat.chatInfo.incognito ? .indigo : theme.colors.primary)
|
||||
@@ -335,7 +335,7 @@ struct ChatPreviewView: View {
|
||||
chatPreviewInfoText("connecting…")
|
||||
}
|
||||
}
|
||||
case let .group(groupInfo):
|
||||
case let .group(groupInfo, _):
|
||||
switch (groupInfo.membership.memberStatus) {
|
||||
case .memRejected: chatPreviewInfoText("rejected")
|
||||
case .memInvited: groupInvitationPreviewText(groupInfo)
|
||||
@@ -445,6 +445,8 @@ struct ChatPreviewView: View {
|
||||
ProgressView()
|
||||
} else if chat.chatStats.reportsCount > 0 {
|
||||
groupReportsIcon(size: size * 0.8)
|
||||
} else if chat.chatStats.supportChatsUnreadCount > 0 {
|
||||
GroupSupportUnreadIcon(size: size * 0.8)
|
||||
} else {
|
||||
incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size)
|
||||
}
|
||||
@@ -498,6 +500,19 @@ struct ChatPreviewView: View {
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
|
||||
struct GroupSupportUnreadIcon: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var size: CGFloat
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: "flag")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
func smallContentPreview(size: CGFloat, _ view: @escaping () -> some View) -> some View {
|
||||
view()
|
||||
.frame(width: size, height: size)
|
||||
|
||||
@@ -66,6 +66,8 @@ struct LocalAuthView: View {
|
||||
m.chatId = nil
|
||||
ItemsModel.shared.reversedChatItems = []
|
||||
ItemsModel.shared.chatState.clear()
|
||||
ChatModel.shared.secondaryIM?.reversedChatItems = []
|
||||
ChatModel.shared.secondaryIM?.chatState.clear()
|
||||
m.updateChats([])
|
||||
m.users = []
|
||||
_ = kcAppPassword.set(password)
|
||||
|
||||
@@ -193,7 +193,7 @@ struct AddGroupView: View {
|
||||
Task {
|
||||
await m.loadGroupMembers(gInfo)
|
||||
}
|
||||
let c = Chat(chatInfo: .group(groupInfo: gInfo), chatItems: [])
|
||||
let c = Chat(chatInfo: .group(groupInfo: gInfo, groupChatScope: nil), chatItems: [])
|
||||
m.addChat(c)
|
||||
withAnimation {
|
||||
groupInfo = gInfo
|
||||
|
||||
@@ -367,13 +367,13 @@ struct ChatThemePreview: View {
|
||||
let alice = ChatItem.getSample(1, CIDirection.directRcv, Date.now, NSLocalizedString("Good afternoon!", comment: "message preview"))
|
||||
let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir))
|
||||
HStack {
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: alice, scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: ItemsModel.shared, chatItem: alice, scrollToItemId: { _ in })
|
||||
.modifier(ChatItemClipped(alice, tailVisible: true))
|
||||
Spacer()
|
||||
}
|
||||
HStack {
|
||||
Spacer()
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: bob, scrollToItemId: { _ in })
|
||||
ChatItemView(chat: Chat.sampleData, im: ItemsModel.shared, chatItem: bob, scrollToItemId: { _ in })
|
||||
.modifier(ChatItemClipped(bob, tailVisible: true))
|
||||
.frame(alignment: .trailing)
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab"
|
||||
let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown"
|
||||
let DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE = "showHiddenProfilesNotice"
|
||||
let DEFAULT_SHOW_MUTE_PROFILE_ALERT = "showMuteProfileAlert"
|
||||
let DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT = "showReportsInSupportChatAlert"
|
||||
let DEFAULT_WHATS_NEW_VERSION = "defaultWhatsNewVersion"
|
||||
let DEFAULT_ONBOARDING_STAGE = "onboardingStage"
|
||||
let DEFAULT_MIGRATION_TO_STAGE = "migrationToStage"
|
||||
@@ -118,6 +119,7 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false,
|
||||
DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE: true,
|
||||
DEFAULT_SHOW_MUTE_PROFILE_ALERT: true,
|
||||
DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT: true,
|
||||
DEFAULT_ONBOARDING_STAGE: OnboardingStage.onboardingComplete.rawValue,
|
||||
DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME: 300,
|
||||
DEFAULT_SHOW_UNREAD_AND_FAVORITES: false,
|
||||
@@ -145,6 +147,7 @@ let hintDefaults = [
|
||||
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN,
|
||||
DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE,
|
||||
DEFAULT_SHOW_MUTE_PROFILE_ALERT,
|
||||
DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT,
|
||||
DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE,
|
||||
DEFAULT_SHOW_DELETE_CONTACT_NOTICE
|
||||
]
|
||||
|
||||
@@ -67,6 +67,7 @@ func apiSendMessages(
|
||||
: SEChatCommand.apiSendMessages(
|
||||
type: chatInfo.chatType,
|
||||
id: chatInfo.apiId,
|
||||
scope: chatInfo.groupChatScope(),
|
||||
live: false,
|
||||
ttl: nil,
|
||||
composedMessages: composedMessages
|
||||
@@ -123,7 +124,7 @@ enum SEChatCommand: ChatCmdProtocol {
|
||||
case apiSetEncryptLocalFiles(enable: Bool)
|
||||
case apiGetChats(userId: Int64)
|
||||
case apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage])
|
||||
case apiSendMessages(type: ChatType, id: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
|
||||
case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
|
||||
|
||||
var cmdString: String {
|
||||
switch self {
|
||||
@@ -139,15 +140,27 @@ enum SEChatCommand: ChatCmdProtocol {
|
||||
case let .apiCreateChatItems(noteFolderId, composedMessages):
|
||||
let msgs = encodeJSON(composedMessages)
|
||||
return "/_create *\(noteFolderId) json \(msgs)"
|
||||
case let .apiSendMessages(type, id, live, ttl, composedMessages):
|
||||
case let .apiSendMessages(type, id, scope, live, ttl, composedMessages):
|
||||
let msgs = encodeJSON(composedMessages)
|
||||
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
|
||||
return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
|
||||
return "/_send \(ref(type, id, scope: scope)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
|
||||
}
|
||||
}
|
||||
|
||||
func ref(_ type: ChatType, _ id: Int64) -> String {
|
||||
"\(type.rawValue)\(id)"
|
||||
func ref(_ type: ChatType, _ id: Int64, scope: GroupChatScope?) -> String {
|
||||
"\(type.rawValue)\(id)\(scopeRef(scope: scope))"
|
||||
}
|
||||
|
||||
func scopeRef(scope: GroupChatScope?) -> String {
|
||||
switch (scope) {
|
||||
case .none: ""
|
||||
case let .memberSupport(groupMemberId_):
|
||||
if let groupMemberId = groupMemberId_ {
|
||||
"(_support:\(groupMemberId))"
|
||||
} else {
|
||||
"(_support)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +168,11 @@
|
||||
648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */; };
|
||||
649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; };
|
||||
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
|
||||
64A779F62DBFB9F200FDEF2F /* MemberAdmissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A779F52DBFB9F200FDEF2F /* MemberAdmissionView.swift */; };
|
||||
64A779F82DBFDBF200FDEF2F /* MemberSupportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A779F72DBFDBF200FDEF2F /* MemberSupportView.swift */; };
|
||||
64A779FC2DC1040000FDEF2F /* SecondaryChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A779FB2DC1040000FDEF2F /* SecondaryChatView.swift */; };
|
||||
64A779FE2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A779FD2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift */; };
|
||||
64A77A022DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */; };
|
||||
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
|
||||
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
|
||||
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
|
||||
@@ -527,6 +532,11 @@
|
||||
6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = "<group>"; };
|
||||
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
|
||||
64A779F52DBFB9F200FDEF2F /* MemberAdmissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberAdmissionView.swift; sourceTree = "<group>"; };
|
||||
64A779F72DBFDBF200FDEF2F /* MemberSupportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberSupportView.swift; sourceTree = "<group>"; };
|
||||
64A779FB2DC1040000FDEF2F /* SecondaryChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryChatView.swift; sourceTree = "<group>"; };
|
||||
64A779FD2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberSupportChatToolbar.swift; sourceTree = "<group>"; };
|
||||
64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextPendingMemberActionsView.swift; sourceTree = "<group>"; };
|
||||
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
|
||||
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
|
||||
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
|
||||
@@ -1076,6 +1086,7 @@
|
||||
644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */,
|
||||
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */,
|
||||
6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */,
|
||||
64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */,
|
||||
);
|
||||
path = ComposeMessage;
|
||||
sourceTree = "<group>";
|
||||
@@ -1117,6 +1128,10 @@
|
||||
6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */,
|
||||
1841516F0CE5992B0EDFB377 /* GroupWelcomeView.swift */,
|
||||
B70CE9E52D4BE5930080F36D /* GroupMentions.swift */,
|
||||
64A779F52DBFB9F200FDEF2F /* MemberAdmissionView.swift */,
|
||||
64A779F72DBFDBF200FDEF2F /* MemberSupportView.swift */,
|
||||
64A779FB2DC1040000FDEF2F /* SecondaryChatView.swift */,
|
||||
64A779FD2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift */,
|
||||
);
|
||||
path = Group;
|
||||
sourceTree = "<group>";
|
||||
@@ -1431,8 +1446,10 @@
|
||||
640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */,
|
||||
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */,
|
||||
640743612CD360E600158442 /* ChooseServerOperators.swift in Sources */,
|
||||
64A779FE2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift in Sources */,
|
||||
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */,
|
||||
5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */,
|
||||
64A779F82DBFDBF200FDEF2F /* MemberSupportView.swift in Sources */,
|
||||
5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */,
|
||||
5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */,
|
||||
B79ADAFF2CE4EF930083DFFD /* AddressCreationCard.swift in Sources */,
|
||||
@@ -1467,6 +1484,7 @@
|
||||
5CB634AF29E4BB7D0066AD6B /* SetAppPasscodeView.swift in Sources */,
|
||||
5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */,
|
||||
5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */,
|
||||
64A779FC2DC1040000FDEF2F /* SecondaryChatView.swift in Sources */,
|
||||
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */,
|
||||
648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */,
|
||||
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */,
|
||||
@@ -1509,6 +1527,7 @@
|
||||
5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */,
|
||||
647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */,
|
||||
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */,
|
||||
64A77A022DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift in Sources */,
|
||||
8C74C3EA2C1B90AF00039E77 /* ThemeManager.swift in Sources */,
|
||||
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */,
|
||||
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */,
|
||||
@@ -1587,6 +1606,7 @@
|
||||
1841560FD1CD447955474C1D /* UserProfilesView.swift in Sources */,
|
||||
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */,
|
||||
8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */,
|
||||
64A779F62DBFB9F200FDEF2F /* MemberAdmissionView.swift in Sources */,
|
||||
18415C6C56DBCEC2CBBD2F11 /* WebRTCClient.swift in Sources */,
|
||||
8CB15EA02CFDA30600C28209 /* ChatItemsMerger.swift in Sources */,
|
||||
184152CEF68D2336FC2EBCB0 /* CallViewRenderers.swift in Sources */,
|
||||
|
||||
@@ -1197,7 +1197,7 @@ public enum GroupFeatureEnabled: String, Codable, Identifiable, Hashable {
|
||||
|
||||
public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
case direct(contact: Contact)
|
||||
case group(groupInfo: GroupInfo)
|
||||
case group(groupInfo: GroupInfo, groupChatScope: GroupChatScopeInfo?)
|
||||
case local(noteFolder: NoteFolder)
|
||||
case contactRequest(contactRequest: UserContactRequest)
|
||||
case contactConnection(contactConnection: PendingContactConnection)
|
||||
@@ -1211,7 +1211,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
|
||||
@@ -1224,7 +1224,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
|
||||
@@ -1237,7 +1237,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
|
||||
@@ -1250,7 +1250,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
|
||||
@@ -1263,7 +1263,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
|
||||
@@ -1276,7 +1276,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
|
||||
@@ -1302,7 +1302,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
|
||||
@@ -1315,7 +1315,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
|
||||
@@ -1337,7 +1337,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.sendMsgEnabled
|
||||
case let .group(groupInfo): return groupInfo.sendMsgEnabled
|
||||
case let .group(groupInfo, _): return groupInfo.sendMsgEnabled
|
||||
case let .local(noteFolder): return noteFolder.sendMsgEnabled
|
||||
case let .contactRequest(contactRequest): return contactRequest.sendMsgEnabled
|
||||
case let .contactConnection(contactConnection): return contactConnection.sendMsgEnabled
|
||||
@@ -1350,7 +1350,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
|
||||
@@ -1375,7 +1375,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
|
||||
}
|
||||
}
|
||||
@@ -1392,7 +1392,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
case .voice: return cups.voice.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
|
||||
@@ -1415,7 +1415,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:
|
||||
@@ -1440,7 +1440,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 {
|
||||
@@ -1471,7 +1471,14 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
return .other
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func groupChatScope() -> GroupChatScope? {
|
||||
switch self {
|
||||
case let .group(_, groupChatScope): groupChatScope?.toChatScope()
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
public func ntfsEnabled(chatItem: ChatItem) -> Bool {
|
||||
ntfsEnabled(chatItem.meta.userMention)
|
||||
}
|
||||
@@ -1487,7 +1494,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
|
||||
}
|
||||
}
|
||||
@@ -1503,7 +1510,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
|
||||
}
|
||||
}
|
||||
@@ -1511,7 +1518,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
var createdAt: Date {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.createdAt
|
||||
case let .group(groupInfo): return groupInfo.createdAt
|
||||
case let .group(groupInfo, _): return groupInfo.createdAt
|
||||
case let .local(noteFolder): return noteFolder.createdAt
|
||||
case let .contactRequest(contactRequest): return contactRequest.createdAt
|
||||
case let .contactConnection(contactConnection): return contactConnection.createdAt
|
||||
@@ -1522,7 +1529,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
public var updatedAt: Date {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.updatedAt
|
||||
case let .group(groupInfo): return groupInfo.updatedAt
|
||||
case let .group(groupInfo, _): return groupInfo.updatedAt
|
||||
case let .local(noteFolder): return noteFolder.updatedAt
|
||||
case let .contactRequest(contactRequest): return contactRequest.updatedAt
|
||||
case let .contactConnection(contactConnection): return contactConnection.updatedAt
|
||||
@@ -1533,7 +1540,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
|
||||
@@ -1549,7 +1556,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 {
|
||||
@@ -1569,7 +1576,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),
|
||||
group: ChatInfo.group(groupInfo: GroupInfo.sampleData, groupChatScope: nil),
|
||||
local: ChatInfo.local(noteFolder: NoteFolder.sampleData),
|
||||
contactRequest: ChatInfo.contactRequest(contactRequest: UserContactRequest.sampleData),
|
||||
contactConnection: ChatInfo.contactConnection(contactConnection: PendingContactConnection.getSampleData())
|
||||
@@ -1599,10 +1606,18 @@ public struct ChatData: Decodable, Identifiable, Hashable, ChatLike {
|
||||
}
|
||||
|
||||
public struct ChatStats: Decodable, Hashable {
|
||||
public init(unreadCount: Int = 0, unreadMentions: Int = 0, reportsCount: Int = 0, minUnreadItemId: Int64 = 0, unreadChat: Bool = false) {
|
||||
public init(
|
||||
unreadCount: Int = 0,
|
||||
unreadMentions: Int = 0,
|
||||
reportsCount: Int = 0,
|
||||
supportChatsUnreadCount: Int = 0,
|
||||
minUnreadItemId: Int64 = 0,
|
||||
unreadChat: Bool = false
|
||||
) {
|
||||
self.unreadCount = unreadCount
|
||||
self.unreadMentions = unreadMentions
|
||||
self.reportsCount = reportsCount
|
||||
self.supportChatsUnreadCount = supportChatsUnreadCount
|
||||
self.minUnreadItemId = minUnreadItemId
|
||||
self.unreadChat = unreadChat
|
||||
}
|
||||
@@ -1611,11 +1626,39 @@ public struct ChatStats: Decodable, Hashable {
|
||||
public var unreadMentions: Int = 0
|
||||
// actual only via getChats() and getChat(.initial), otherwise, zero
|
||||
public var reportsCount: Int = 0
|
||||
// actual only via getChats() and getChat(.initial), otherwise, zero
|
||||
public var supportChatsUnreadCount: Int = 0
|
||||
public var minUnreadItemId: Int64 = 0
|
||||
// actual only via getChats(), otherwise, false
|
||||
public var unreadChat: Bool = false
|
||||
}
|
||||
|
||||
public enum GroupChatScope: Decodable {
|
||||
case memberSupport(groupMemberId_: Int64?)
|
||||
}
|
||||
|
||||
public func sameChatScope(_ scope1: GroupChatScope, _ scope2: GroupChatScope) -> Bool {
|
||||
switch (scope1, scope2) {
|
||||
case let (.memberSupport(groupMemberId1_), .memberSupport(groupMemberId2_)):
|
||||
return groupMemberId1_ == groupMemberId2_
|
||||
}
|
||||
}
|
||||
|
||||
public enum GroupChatScopeInfo: Decodable, Hashable {
|
||||
case memberSupport(groupMember_: GroupMember?)
|
||||
|
||||
public func toChatScope() -> GroupChatScope {
|
||||
switch self {
|
||||
case let .memberSupport(groupMember_):
|
||||
if let groupMember = groupMember_ {
|
||||
return .memberSupport(groupMemberId_: groupMember.groupMemberId)
|
||||
} else {
|
||||
return .memberSupport(groupMemberId_: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Contact: Identifiable, Decodable, NamedChat, Hashable {
|
||||
public var contactId: Int64
|
||||
var localDisplayName: ContactName
|
||||
@@ -2010,6 +2053,10 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
|
||||
return membership.memberRole >= .admin && membership.memberActive
|
||||
}
|
||||
|
||||
public var canModerate: Bool {
|
||||
return membership.memberRole >= .moderator && membership.memberActive
|
||||
}
|
||||
|
||||
public static let sampleData = GroupInfo(
|
||||
groupId: 1,
|
||||
localDisplayName: "team",
|
||||
@@ -2030,12 +2077,20 @@ public struct GroupRef: Decodable, Hashable {
|
||||
}
|
||||
|
||||
public struct GroupProfile: Codable, NamedChat, Hashable {
|
||||
public init(displayName: String, fullName: String, description: String? = nil, image: String? = nil, groupPreferences: GroupPreferences? = nil) {
|
||||
public init(
|
||||
displayName: String,
|
||||
fullName: String,
|
||||
description: String? = nil,
|
||||
image: String? = nil,
|
||||
groupPreferences: GroupPreferences? = nil,
|
||||
memberAdmission: GroupMemberAdmission? = nil
|
||||
) {
|
||||
self.displayName = displayName
|
||||
self.fullName = fullName
|
||||
self.description = description
|
||||
self.image = image
|
||||
self.groupPreferences = groupPreferences
|
||||
self.memberAdmission = memberAdmission
|
||||
}
|
||||
|
||||
public var displayName: String
|
||||
@@ -2043,6 +2098,7 @@ public struct GroupProfile: Codable, NamedChat, Hashable {
|
||||
public var description: String?
|
||||
public var image: String?
|
||||
public var groupPreferences: GroupPreferences?
|
||||
public var memberAdmission: GroupMemberAdmission?
|
||||
public var localAlias: String { "" }
|
||||
|
||||
public static let sampleData = GroupProfile(
|
||||
@@ -2051,6 +2107,34 @@ public struct GroupProfile: Codable, NamedChat, Hashable {
|
||||
)
|
||||
}
|
||||
|
||||
public struct GroupMemberAdmission: Codable, Hashable {
|
||||
public var review: MemberCriteria?
|
||||
|
||||
public init(
|
||||
review: MemberCriteria? = nil
|
||||
) {
|
||||
self.review = review
|
||||
}
|
||||
|
||||
public static let sampleData = GroupMemberAdmission(
|
||||
review: .all
|
||||
)
|
||||
}
|
||||
|
||||
public enum MemberCriteria: String, Codable, Identifiable, Hashable {
|
||||
case all
|
||||
|
||||
public static var values: [MemberCriteria] { [.all] }
|
||||
|
||||
public var id: Self { self }
|
||||
|
||||
public var text: String {
|
||||
switch self {
|
||||
case .all: return NSLocalizedString("all", comment: "member criteria value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct BusinessChatInfo: Decodable, Hashable {
|
||||
public var chatType: BusinessChatType
|
||||
public var businessId: String
|
||||
@@ -2077,6 +2161,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
|
||||
public var memberContactId: Int64?
|
||||
public var memberContactProfileId: Int64
|
||||
public var activeConn: Connection?
|
||||
public var supportChat: GroupSupportChat?
|
||||
public var memberChatVRange: VersionRange
|
||||
|
||||
public var id: String { "#\(groupId) @\(groupMemberId)" }
|
||||
@@ -2148,6 +2233,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
|
||||
case .memUnknown: return false
|
||||
case .memInvited: return false
|
||||
case .memPendingApproval: return true
|
||||
case .memPendingReview: return true
|
||||
case .memIntroduced: return false
|
||||
case .memIntroInvited: return false
|
||||
case .memAccepted: return false
|
||||
@@ -2167,6 +2253,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
|
||||
case .memUnknown: return false
|
||||
case .memInvited: return false
|
||||
case .memPendingApproval: return false
|
||||
case .memPendingReview: return false
|
||||
case .memIntroduced: return true
|
||||
case .memIntroInvited: return true
|
||||
case .memAccepted: return true
|
||||
@@ -2177,6 +2264,14 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public var memberPending: Bool {
|
||||
switch memberStatus {
|
||||
case .memPendingApproval: return true
|
||||
case .memPendingReview: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
public func canBeRemoved(groupInfo: GroupInfo) -> Bool {
|
||||
let userRole = groupInfo.membership.memberRole
|
||||
return memberStatus != .memRemoved && memberStatus != .memLeft
|
||||
@@ -2230,6 +2325,13 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
|
||||
)
|
||||
}
|
||||
|
||||
public struct GroupSupportChat: Codable, Hashable {
|
||||
public var chatTs: Date
|
||||
public var unread: Int
|
||||
public var memberAttention: Int
|
||||
public var mentions: Int
|
||||
}
|
||||
|
||||
public struct GroupMemberSettings: Codable, Hashable {
|
||||
public var showMessages: Bool
|
||||
}
|
||||
@@ -2299,6 +2401,7 @@ public enum GroupMemberStatus: String, Decodable, Hashable {
|
||||
case memUnknown = "unknown"
|
||||
case memInvited = "invited"
|
||||
case memPendingApproval = "pending_approval"
|
||||
case memPendingReview = "pending_review"
|
||||
case memIntroduced = "introduced"
|
||||
case memIntroInvited = "intro-inv"
|
||||
case memAccepted = "accepted"
|
||||
@@ -2316,6 +2419,7 @@ public enum GroupMemberStatus: String, Decodable, Hashable {
|
||||
case .memUnknown: return "unknown status"
|
||||
case .memInvited: return "invited"
|
||||
case .memPendingApproval: return "pending approval"
|
||||
case .memPendingReview: return "pending review"
|
||||
case .memIntroduced: return "connecting (introduced)"
|
||||
case .memIntroInvited: return "connecting (introduction invitation)"
|
||||
case .memAccepted: return "connecting (accepted)"
|
||||
@@ -2335,6 +2439,7 @@ public enum GroupMemberStatus: String, Decodable, Hashable {
|
||||
case .memUnknown: return "unknown"
|
||||
case .memInvited: return "invited"
|
||||
case .memPendingApproval: return "pending"
|
||||
case .memPendingReview: return "review"
|
||||
case .memIntroduced: return "connecting"
|
||||
case .memIntroInvited: return "connecting"
|
||||
case .memAccepted: return "connecting"
|
||||
@@ -2589,12 +2694,14 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
|
||||
case .userDeleted: nil
|
||||
case .groupDeleted: nil
|
||||
case .memberCreatedContact: nil
|
||||
case .newMemberPendingReview: nil
|
||||
default: .rcvGroupEvent
|
||||
}
|
||||
case let .sndGroupEvent(event):
|
||||
switch event {
|
||||
case .userRole: nil
|
||||
case .userLeft: nil
|
||||
case .userPendingReview: nil
|
||||
default: .sndGroupEvent
|
||||
}
|
||||
default:
|
||||
@@ -2627,6 +2734,8 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
|
||||
switch rcvGroupEvent {
|
||||
case .groupUpdated: return false
|
||||
case .memberConnected: return false
|
||||
case .memberAccepted: return false
|
||||
case .userAccepted: return false
|
||||
case .memberRole: return false
|
||||
case .memberBlocked: return false
|
||||
case .userRole: return true
|
||||
@@ -2638,6 +2747,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
|
||||
case .invitedViaGroupLink: return false
|
||||
case .memberCreatedContact: return false
|
||||
case .memberProfileUpdated: return false
|
||||
case .newMemberPendingReview: return true
|
||||
}
|
||||
case .sndGroupEvent: return false
|
||||
case .rcvConnEvent: return false
|
||||
@@ -2706,12 +2816,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 >= .admin && 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 >= .admin ? (groupInfo, nil) : nil
|
||||
default: return nil
|
||||
@@ -3093,6 +3203,21 @@ public enum CIStatus: Decodable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
// as in corresponds to SENT response from agent, opposed to `sent` which means snd status
|
||||
public var isSent: Bool {
|
||||
switch self {
|
||||
case .sndNew: false
|
||||
case .sndSent: true
|
||||
case .sndRcvd: false
|
||||
case .sndErrorAuth: true
|
||||
case .sndError: true
|
||||
case .sndWarning: true
|
||||
case .rcvNew: false
|
||||
case .rcvRead: false
|
||||
case .invalid: false
|
||||
}
|
||||
}
|
||||
|
||||
public func statusIcon(_ metaColor: Color, _ paleMetaColor: Color, _ primaryColor: Color = .accentColor) -> (Image, Color)? {
|
||||
switch self {
|
||||
case .sndNew: nil
|
||||
@@ -3146,6 +3271,17 @@ public enum CIStatus: Decodable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public func shouldKeepOldSndCIStatus(oldStatus: CIStatus, newStatus: CIStatus) -> Bool {
|
||||
switch (oldStatus, newStatus) {
|
||||
case (.sndRcvd, let new) where !new.isSndRcvd:
|
||||
return true
|
||||
case (let old, .sndNew) where old.isSent:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public enum SndError: Decodable, Hashable {
|
||||
case auth
|
||||
case quota
|
||||
@@ -4045,6 +4181,16 @@ extension MsgContent: Encodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum MsgContentTag: String {
|
||||
case text
|
||||
case link
|
||||
case image
|
||||
case video
|
||||
case voice
|
||||
case file
|
||||
case report
|
||||
}
|
||||
|
||||
public struct FormattedText: Decodable, Hashable {
|
||||
public var text: String
|
||||
public var format: Format?
|
||||
@@ -4376,6 +4522,8 @@ public enum RcvDirectEvent: Decodable, Hashable {
|
||||
public enum RcvGroupEvent: Decodable, Hashable {
|
||||
case memberAdded(groupMemberId: Int64, profile: Profile)
|
||||
case memberConnected
|
||||
case memberAccepted(groupMemberId: Int64, profile: Profile)
|
||||
case userAccepted
|
||||
case memberLeft
|
||||
case memberRole(groupMemberId: Int64, profile: Profile, role: GroupMemberRole)
|
||||
case memberBlocked(groupMemberId: Int64, profile: Profile, blocked: Bool)
|
||||
@@ -4387,12 +4535,16 @@ public enum RcvGroupEvent: Decodable, Hashable {
|
||||
case invitedViaGroupLink
|
||||
case memberCreatedContact
|
||||
case memberProfileUpdated(fromProfile: Profile, toProfile: Profile)
|
||||
case newMemberPendingReview
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case let .memberAdded(_, profile):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("invited %@", comment: "rcv group event chat item"), profile.profileViewName)
|
||||
case .memberConnected: return NSLocalizedString("member connected", comment: "rcv group event chat item")
|
||||
case let .memberAccepted(_, profile):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("accepted %@", comment: "rcv group event chat item"), profile.profileViewName)
|
||||
case .userAccepted: return NSLocalizedString("accepted you", comment: "rcv group event chat item")
|
||||
case .memberLeft: return NSLocalizedString("left", comment: "rcv group event chat item")
|
||||
case let .memberRole(_, profile, role):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("changed role of %@ to %@", comment: "rcv group event chat item"), profile.profileViewName, role.text)
|
||||
@@ -4412,6 +4564,7 @@ public enum RcvGroupEvent: Decodable, Hashable {
|
||||
case .invitedViaGroupLink: return NSLocalizedString("invited via your group link", comment: "rcv group event chat item")
|
||||
case .memberCreatedContact: return NSLocalizedString("connected directly", comment: "rcv group event chat item")
|
||||
case let .memberProfileUpdated(fromProfile, toProfile): return profileUpdatedText(fromProfile, toProfile)
|
||||
case .newMemberPendingReview: return NSLocalizedString("New member wants to join the group.", comment: "rcv group event chat item")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4436,6 +4589,7 @@ public enum SndGroupEvent: Decodable, Hashable {
|
||||
case memberDeleted(groupMemberId: Int64, profile: Profile)
|
||||
case userLeft
|
||||
case groupUpdated(groupProfile: GroupProfile)
|
||||
case userPendingReview
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
@@ -4453,6 +4607,8 @@ public enum SndGroupEvent: Decodable, Hashable {
|
||||
return String.localizedStringWithFormat(NSLocalizedString("you removed %@", comment: "snd group event chat item"), profile.profileViewName)
|
||||
case .userLeft: return NSLocalizedString("you left", comment: "snd group event chat item")
|
||||
case .groupUpdated: return NSLocalizedString("group profile updated", comment: "snd group event chat item")
|
||||
case .userPendingReview:
|
||||
return NSLocalizedString("Please wait for group moderators to review your request to join the group.", comment: "snd group event chat item")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,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
|
||||
@@ -83,7 +83,7 @@ public func foundChat(_ chat: ChatLike, _ searchStr: String) -> Bool {
|
||||
private func canForwardToChat(_ cInfo: ChatInfo) -> Bool {
|
||||
switch cInfo {
|
||||
case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv
|
||||
case let .group(groupInfo): groupInfo.sendMsgEnabled
|
||||
case let .group(groupInfo, _): groupInfo.sendMsgEnabled
|
||||
case let .local(noteFolder): noteFolder.sendMsgEnabled
|
||||
case .contactRequest: false
|
||||
case .contactConnection: false
|
||||
@@ -94,7 +94,7 @@ private func canForwardToChat(_ cInfo: ChatInfo) -> Bool {
|
||||
public func chatIconName(_ cInfo: ChatInfo) -> String {
|
||||
switch cInfo {
|
||||
case .direct: "person.crop.circle.fill"
|
||||
case let .group(groupInfo):
|
||||
case let .group(groupInfo, _):
|
||||
switch groupInfo.businessChat?.chatType {
|
||||
case .none: "person.2.circle.fill"
|
||||
case .business: "briefcase.circle.fill"
|
||||
|
||||
@@ -62,7 +62,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):"
|
||||
|
||||
@@ -3103,6 +3103,7 @@ sealed class CIStatus {
|
||||
@Serializable @SerialName("rcvRead") class RcvRead: CIStatus()
|
||||
@Serializable @SerialName("invalid") class Invalid(val text: String): CIStatus()
|
||||
|
||||
// as in corresponds to SENT response from agent
|
||||
fun isSent(): Boolean = when(this) {
|
||||
is SndNew -> false
|
||||
is SndSent -> true
|
||||
|
||||
+1
-1
@@ -165,7 +165,7 @@ fun SupportChatRow(member: GroupMember) {
|
||||
fun memberStatus(): String {
|
||||
return if (member.activeConn?.connDisabled == true) {
|
||||
generalGetString(MR.strings.member_info_member_disabled)
|
||||
} else if (member.activeConn?.connDisabled == true) {
|
||||
} else if (member.activeConn?.connInactive == true) {
|
||||
generalGetString(MR.strings.member_info_member_inactive)
|
||||
} else if (member.memberPending) {
|
||||
member.memberStatus.text
|
||||
|
||||
Reference in New Issue
Block a user