mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-30 13:19:54 +00:00
Compare commits
11 commits
v6.4.0-bet
...
stable
Author | SHA1 | Date | |
---|---|---|---|
|
3d22b738d8 | ||
|
c08189108e | ||
|
442d9afc4b | ||
|
a593557c21 | ||
|
07abe24e18 | ||
|
5f6595dda9 | ||
|
6fdd50efb9 | ||
|
50dfda6c09 | ||
|
ea1a81fcac | ||
|
cf0639bf28 | ||
|
7b362ff655 |
129 changed files with 2930 additions and 7562 deletions
20
README.md
20
README.md
|
@ -10,7 +10,7 @@
|
|||
|
||||
# SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design!
|
||||
|
||||
[<img src="./images/trail-of-bits.jpg" height="100">](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) [<img src="./images/privacy-guides.jpg" height="80">](https://www.privacyguides.org/en/real-time-communication/#simplex-chat) [<img src="./images/kuketz-blog.jpg" height="80">](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/)
|
||||
[<img src="./images/trail-of-bits.jpg" height="80">](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) [<img src="./images/privacy-guides.jpg" height="64">](https://www.privacyguides.org/en/real-time-communication/#simplex-chat) [<img src="./images/whonix-logo.jpg" height="64">](https://www.whonix.org/wiki/Chat#Recommendation) [<img src="./images/kuketz-blog.jpg" height="64">](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/)
|
||||
|
||||
## Welcome to SimpleX Chat!
|
||||
|
||||
|
@ -110,6 +110,15 @@ After you connect, you can [verify connection security code](./blog/20230103-sim
|
|||
|
||||
Read about the app features and settings in the new [User guide](./docs/guide/README.md).
|
||||
|
||||
## Contribute
|
||||
|
||||
We would love to have you join the development! You can help us with:
|
||||
|
||||
- [share the color theme](./docs/THEMES.md) you use in Android app!
|
||||
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
|
||||
- contributing to SimpleX Chat knowledge-base.
|
||||
- developing features - please connect to us via chat so we can help you get started.
|
||||
|
||||
## Help translating SimpleX Chat
|
||||
|
||||
Thanks to our users and [Weblate](https://hosted.weblate.org/engage/simplex-chat/), SimpleX Chat apps, website and documents are translated to many other languages.
|
||||
|
@ -141,15 +150,6 @@ Join our translators to help SimpleX grow!
|
|||
|
||||
Languages in progress: Arabic, Japanese, Korean, Portuguese and [others](https://hosted.weblate.org/projects/simplex-chat/#languages). We will be adding more languages as some of the already added are completed – please suggest new languages, review the [translation guide](./docs/TRANSLATIONS.md) and get in touch with us!
|
||||
|
||||
## Contribute
|
||||
|
||||
We would love to have you join the development! You can help us with:
|
||||
|
||||
- [share the color theme](./docs/THEMES.md) you use in Android app!
|
||||
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
|
||||
- contributing to SimpleX Chat knowledge-base.
|
||||
- developing features - please connect to us via chat so we can help you get started.
|
||||
|
||||
## Please support us with your donations
|
||||
|
||||
Huge thank you to everybody who donated to SimpleX Chat!
|
||||
|
|
|
@ -39,9 +39,9 @@ enum ChatCommand: ChatCmdProtocol {
|
|||
case apiGetSettings(settings: AppSettings)
|
||||
case apiGetChatTags(userId: Int64)
|
||||
case apiGetChats(userId: Int64)
|
||||
case apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag?, pagination: ChatPagination, search: String)
|
||||
case apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64)
|
||||
case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
|
||||
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 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, scope: GroupChatScope?, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool)
|
||||
case apiDeleteChatItem(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64], mode: CIDeleteMode)
|
||||
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool)
|
||||
case apiDeleteChatItem(type: ChatType, id: Int64, 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, scope: GroupChatScope?, itemId: Int64, add: Bool, reaction: MsgReaction)
|
||||
case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction)
|
||||
case apiGetReactionMembers(userId: Int64, groupId: Int64, itemId: Int64, reaction: MsgReaction)
|
||||
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 apiPlanForwardChatItems(toChatType: ChatType, toChatId: Int64, itemIds: [Int64])
|
||||
case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?)
|
||||
case apiGetNtfToken
|
||||
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
|
||||
case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
|
||||
|
@ -68,7 +68,6 @@ 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)
|
||||
|
@ -148,8 +147,8 @@ enum ChatCommand: ChatCmdProtocol {
|
|||
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
|
||||
// WebRTC calls /
|
||||
case apiGetNetworkStatuses
|
||||
case apiChatRead(type: ChatType, id: Int64, scope: GroupChatScope?)
|
||||
case apiChatItemsRead(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64])
|
||||
case apiChatRead(type: ChatType, id: Int64)
|
||||
case apiChatItemsRead(type: ChatType, id: Int64, 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?)
|
||||
|
@ -210,16 +209,15 @@ enum ChatCommand: ChatCmdProtocol {
|
|||
case let .apiGetSettings(settings): return "/_get app settings \(encodeJSON(settings))"
|
||||
case let .apiGetChatTags(userId): return "/_get tags \(userId)"
|
||||
case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on"
|
||||
case let .apiGetChat(chatId, scope, contentTag, pagination, search):
|
||||
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):
|
||||
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):
|
||||
let msgs = encodeJSON(composedMessages)
|
||||
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
|
||||
return "/_send \(ref(type, id, scope: scope)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
|
||||
return "/_send \(ref(type, id)) 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, scope: nil)) \(tagIds.map({ "\($0)" }).joined(separator: ","))"
|
||||
case let .apiSetChatTags(type, id, tagIds): return "/_tags \(ref(type, id)) \(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: ","))"
|
||||
|
@ -228,17 +226,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, 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 .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 .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, scope, itemId, add, reaction): return "/_reaction \(ref(type, id, scope: scope)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))"
|
||||
case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(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, 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):
|
||||
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):
|
||||
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
|
||||
return "/_forward \(ref(toChatType, toChatId, scope: toScope)) \(ref(fromChatType, fromChatId, scope: fromScope)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) ttl=\(ttlStr)"
|
||||
return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(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)"
|
||||
|
@ -249,7 +247,6 @@ 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))"
|
||||
|
@ -273,13 +270,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, scope: nil)) \(chatItemTTLStr(seconds: seconds))"
|
||||
case let .apiSetChatTTL(userId, type, id, seconds): return "/_ttl \(userId) \(ref(type, id)) \(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, scope: nil)) \(encodeJSON(chatSettings))"
|
||||
case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(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)"
|
||||
|
@ -311,8 +308,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, scope: nil)) \(chatDeleteMode.cmdString)"
|
||||
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id, scope: nil))"
|
||||
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 .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))"
|
||||
|
@ -337,9 +334,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, 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 .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 .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)"
|
||||
|
@ -424,7 +421,6 @@ 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"
|
||||
|
@ -527,20 +523,8 @@ enum ChatCommand: ChatCmdProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
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 ref(_ type: ChatType, _ id: Int64) -> String {
|
||||
"\(type.rawValue)\(id)"
|
||||
}
|
||||
|
||||
func joinedIds(_ ids: [Int64]) -> String {
|
||||
|
@ -727,7 +711,6 @@ enum ChatResponse1: Decodable, ChatAPIResult {
|
|||
case contactDeleted(user: UserRef, contact: Contact)
|
||||
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
||||
case groupDeletedUser(user: UserRef, groupInfo: GroupInfo)
|
||||
case itemsReadForChat(user: UserRef, chatInfo: ChatInfo)
|
||||
case chatCleared(user: UserRef, chatInfo: ChatInfo)
|
||||
case userProfileNoChange(user: User)
|
||||
case userProfileUpdated(user: User, fromProfile: Profile, toProfile: Profile, updateSummary: UserProfileUpdateSummary)
|
||||
|
@ -766,7 +749,6 @@ enum ChatResponse1: Decodable, ChatAPIResult {
|
|||
case .contactDeleted: "contactDeleted"
|
||||
case .contactConnectionDeleted: "contactConnectionDeleted"
|
||||
case .groupDeletedUser: "groupDeletedUser"
|
||||
case .itemsReadForChat: "itemsReadForChat"
|
||||
case .chatCleared: "chatCleared"
|
||||
case .userProfileNoChange: "userProfileNoChange"
|
||||
case .userProfileUpdated: "userProfileUpdated"
|
||||
|
@ -799,7 +781,6 @@ enum ChatResponse1: Decodable, ChatAPIResult {
|
|||
case let .contactDeleted(u, contact): return withUser(u, String(describing: contact))
|
||||
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
|
||||
case let .groupDeletedUser(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .itemsReadForChat(u, chatInfo): return withUser(u, String(describing: chatInfo))
|
||||
case let .chatCleared(u, chatInfo): return withUser(u, String(describing: chatInfo))
|
||||
case .userProfileNoChange: return noDetails
|
||||
case let .userProfileUpdated(u, _, toProfile, _): return withUser(u, String(describing: toProfile))
|
||||
|
@ -850,7 +831,6 @@ enum ChatResponse2: Decodable, ChatAPIResult {
|
|||
case userDeletedMembers(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], withMessages: Bool)
|
||||
case leftMemberUser(user: UserRef, groupInfo: GroupInfo)
|
||||
case groupMembers(user: UserRef, group: SimpleXChat.Group)
|
||||
case memberAccepted(user: UserRef, groupInfo: GroupInfo, member: GroupMember)
|
||||
case membersRoleUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], toRole: GroupMemberRole)
|
||||
case membersBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], blocked: Bool)
|
||||
case groupUpdated(user: UserRef, toGroup: GroupInfo)
|
||||
|
@ -899,7 +879,6 @@ enum ChatResponse2: Decodable, ChatAPIResult {
|
|||
case .userDeletedMembers: "userDeletedMembers"
|
||||
case .leftMemberUser: "leftMemberUser"
|
||||
case .groupMembers: "groupMembers"
|
||||
case .memberAccepted: "memberAccepted"
|
||||
case .membersRoleUser: "membersRoleUser"
|
||||
case .membersBlockedForAllUser: "membersBlockedForAllUser"
|
||||
case .groupUpdated: "groupUpdated"
|
||||
|
@ -944,7 +923,6 @@ enum ChatResponse2: Decodable, ChatAPIResult {
|
|||
case let .userDeletedMembers(u, groupInfo, members, withMessages): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\nwithMessages: \(withMessages)")
|
||||
case let .leftMemberUser(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .groupMembers(u, group): return withUser(u, String(describing: group))
|
||||
case let .memberAccepted(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .membersRoleUser(u, groupInfo, members, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\ntoRole: \(toRole)")
|
||||
case let .membersBlockedForAllUser(u, groupInfo, members, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(members)\nblocked: \(blocked)")
|
||||
case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
|
||||
|
@ -1010,7 +988,6 @@ enum ChatEvent: Decodable, ChatAPIResult {
|
|||
case groupLinkConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember)
|
||||
case businessLinkConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, fromContact: Contact)
|
||||
case joinedGroupMemberConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, 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 memberBlockedForAll(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, blocked: Bool)
|
||||
case deletedMemberUser(user: UserRef, groupInfo: GroupInfo, member: GroupMember, withMessages: Bool)
|
||||
|
@ -1087,7 +1064,6 @@ enum ChatEvent: Decodable, ChatAPIResult {
|
|||
case .groupLinkConnecting: "groupLinkConnecting"
|
||||
case .businessLinkConnecting: "businessLinkConnecting"
|
||||
case .joinedGroupMemberConnecting: "joinedGroupMemberConnecting"
|
||||
case .memberAcceptedByOther: "memberAcceptedByOther"
|
||||
case .memberRole: "memberRole"
|
||||
case .memberBlockedForAll: "memberBlockedForAll"
|
||||
case .deletedMemberUser: "deletedMemberUser"
|
||||
|
@ -1168,7 +1144,6 @@ enum ChatEvent: Decodable, ChatAPIResult {
|
|||
case let .groupLinkConnecting(u, groupInfo, hostMember): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(String(describing: hostMember))")
|
||||
case let .businessLinkConnecting(u, groupInfo, hostMember, fromContact): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(String(describing: hostMember))\nfromContact: \(String(describing: fromContact))")
|
||||
case let .joinedGroupMemberConnecting(u, groupInfo, hostMember, member): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(hostMember)\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 .memberBlockedForAll(u, groupInfo, byMember, member, blocked): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\nmember: \(member)\nblocked: \(blocked)")
|
||||
case let .deletedMemberUser(u, groupInfo, member, withMessages): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nwithMessages: \(withMessages)")
|
||||
|
|
|
@ -52,26 +52,8 @@ 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(secondaryIMFilter: nil)
|
||||
public var secondaryIMFilter: SecondaryItemsModelFilter?
|
||||
public var preloadState = PreloadState()
|
||||
static let shared = ItemsModel()
|
||||
private let publisher = ObservableObjectPublisher()
|
||||
private var bag = Set<AnyCancellable>()
|
||||
var reversedChatItems: [ChatItem] = [] {
|
||||
|
@ -95,20 +77,13 @@ class ItemsModel: ObservableObject {
|
|||
chatState.splits.isEmpty || chatState.splits.first != reversedChatItems.first?.id
|
||||
}
|
||||
|
||||
init(secondaryIMFilter: SecondaryItemsModelFilter? = nil) {
|
||||
self.secondaryIMFilter = secondaryIMFilter
|
||||
init() {
|
||||
publisher
|
||||
.throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true)
|
||||
.sink { self.objectWillChange.send() }
|
||||
.store(in: &bag)
|
||||
}
|
||||
|
||||
static func loadSecondaryChat(_ chatId: ChatId, chatFilter: SecondaryItemsModelFilter, willNavigate: @escaping () -> Void = {}) {
|
||||
let im = ItemsModel(secondaryIMFilter: chatFilter)
|
||||
ChatModel.shared.secondaryIM = im
|
||||
im.loadOpenChat(chatId, willNavigate: willNavigate)
|
||||
}
|
||||
|
||||
func loadOpenChat(_ chatId: ChatId, willNavigate: @escaping () -> Void = {}) {
|
||||
navigationTimeoutTask?.cancel()
|
||||
loadChatTask?.cancel()
|
||||
|
@ -124,7 +99,7 @@ class ItemsModel: ObservableObject {
|
|||
loadChatTask = Task {
|
||||
await MainActor.run { self.isLoading = true }
|
||||
// try? await Task.sleep(nanoseconds: 1000_000000)
|
||||
await loadChat(chatId: chatId, im: self)
|
||||
await loadChat(chatId: chatId)
|
||||
if !Task.isCancelled {
|
||||
await MainActor.run {
|
||||
self.isLoading = false
|
||||
|
@ -139,7 +114,7 @@ class ItemsModel: ObservableObject {
|
|||
loadChatTask?.cancel()
|
||||
loadChatTask = Task {
|
||||
// try? await Task.sleep(nanoseconds: 1000_000000)
|
||||
await loadChat(chatId: chatId, im: self, openAroundItemId: openAroundItemId, clearItems: openAroundItemId == nil)
|
||||
await loadChat(chatId: chatId, openAroundItemId: openAroundItemId, clearItems: openAroundItemId == nil)
|
||||
if !Task.isCancelled {
|
||||
await MainActor.run {
|
||||
if openAroundItemId == nil {
|
||||
|
@ -149,34 +124,6 @@ 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 {
|
||||
|
@ -340,6 +287,7 @@ 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
|
||||
|
@ -388,10 +336,6 @@ 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
|
||||
@Published var secondaryPendingInviteeChatOpened = false
|
||||
|
||||
static var ok: Bool { ChatModel.shared.chatDbStatus == .ok }
|
||||
|
||||
let ntfEnableLocal = true
|
||||
|
@ -449,7 +393,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
|
||||
|
@ -502,11 +446,7 @@ final class ChatModel: ObservableObject {
|
|||
|
||||
func updateChatInfo(_ cInfo: ChatInfo) {
|
||||
if let i = getChatIndex(cInfo.id) {
|
||||
if case let .group(groupInfo, groupChatScope) = cInfo, groupChatScope != nil {
|
||||
chats[i].chatInfo = .group(groupInfo: groupInfo, groupChatScope: nil)
|
||||
} else {
|
||||
chats[i].chatInfo = cInfo
|
||||
}
|
||||
chats[i].chatInfo = cInfo
|
||||
chats[i].created = Date.now
|
||||
}
|
||||
}
|
||||
|
@ -528,7 +468,7 @@ final class ChatModel: ObservableObject {
|
|||
}
|
||||
|
||||
func updateGroup(_ groupInfo: GroupInfo) {
|
||||
updateChat(.group(groupInfo: groupInfo, groupChatScope: nil))
|
||||
updateChat(.group(groupInfo: groupInfo))
|
||||
}
|
||||
|
||||
private func updateChat(_ cInfo: ChatInfo, addMissing: Bool = true) {
|
||||
|
@ -571,112 +511,77 @@ final class ChatModel: ObservableObject {
|
|||
// }
|
||||
|
||||
func addChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
// updates membersRequireAttention
|
||||
updateChatInfo(cInfo)
|
||||
// mark chat non deleted
|
||||
if case let .direct(contact) = cInfo, contact.chatDeleted {
|
||||
var updatedContact = contact
|
||||
updatedContact.chatDeleted = false
|
||||
updateContact(updatedContact)
|
||||
}
|
||||
// update chat list
|
||||
// update previews
|
||||
if let i = getChatIndex(cInfo.id) {
|
||||
// update preview
|
||||
if cInfo.groupChatScope() == nil || cInfo.groupInfo?.membership.memberPending ?? false {
|
||||
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 {
|
||||
chats[i].chatItems = switch cInfo {
|
||||
case .group:
|
||||
if let currentPreviewItem = chats[i].chatItems.first {
|
||||
if cItem.meta.itemTs >= currentPreviewItem.meta.itemTs {
|
||||
[cItem]
|
||||
} else {
|
||||
[currentPreviewItem]
|
||||
}
|
||||
default:
|
||||
} else {
|
||||
[cItem]
|
||||
}
|
||||
if case .rcvNew = cItem.meta.itemStatus {
|
||||
unreadCollector.changeUnreadCounter(cInfo.id, by: 1, unreadMentions: cItem.meta.userMention ? 1 : 0)
|
||||
}
|
||||
default:
|
||||
[cItem]
|
||||
}
|
||||
if case .rcvNew = cItem.meta.itemStatus {
|
||||
unreadCollector.changeUnreadCounter(cInfo.id, by: 1, unreadMentions: cItem.meta.userMention ? 1 : 0)
|
||||
}
|
||||
// pop chat
|
||||
popChatCollector.throttlePopChat(cInfo.id, currentPosition: i)
|
||||
} else {
|
||||
if cInfo.groupChatScope() == nil {
|
||||
addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
|
||||
} else {
|
||||
addChat(Chat(chatInfo: cInfo, chatItems: []))
|
||||
}
|
||||
addChat(Chat(chatInfo: cInfo, chatItems: [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
|
||||
// add to current chat
|
||||
if chatId == cInfo.id {
|
||||
_ = _upsertChatItem(cInfo, cItem)
|
||||
}
|
||||
}
|
||||
|
||||
func upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
||||
// 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 {
|
||||
// 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) {
|
||||
chat.chatItems = [cItem]
|
||||
}
|
||||
} else {
|
||||
addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
|
||||
itemAdded = true
|
||||
}
|
||||
if cItem.isDeletedContent || cItem.meta.itemDeleted != nil {
|
||||
VoiceItemState.stopVoiceInChatView(cInfo, cItem)
|
||||
chat.chatItems = [cItem]
|
||||
}
|
||||
res = false
|
||||
} else {
|
||||
addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
|
||||
res = true
|
||||
}
|
||||
// update current scope
|
||||
if let ciIM = getCIItemsModel(cInfo, cItem) {
|
||||
itemAdded = _upsertChatItem(ciIM, cInfo, cItem)
|
||||
if cItem.isDeletedContent || cItem.meta.itemDeleted != nil {
|
||||
VoiceItemState.stopVoiceInChatView(cInfo, cItem)
|
||||
}
|
||||
return itemAdded
|
||||
// update current chat
|
||||
return chatId == cInfo.id ? _upsertChatItem(cInfo, cItem) : res
|
||||
}
|
||||
|
||||
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
|
||||
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
||||
if let i = getChatItemIndex(cItem) {
|
||||
_updateChatItem(at: i, with: cItem)
|
||||
ChatItemDummyModel.shared.sendUpdate()
|
||||
return false
|
||||
} else {
|
||||
ciIM.reversedChatItems.insert(cItem, at: hasLiveDummy ? 1 : 0)
|
||||
ciIM.chatState.itemAdded((cItem.id, cItem.isRcvNew), hasLiveDummy ? 1 : 0)
|
||||
ciIM.itemAdded = true
|
||||
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
|
||||
ChatItemDummyModel.shared.sendUpdate()
|
||||
return true
|
||||
}
|
||||
|
@ -690,42 +595,40 @@ final class ChatModel: ObservableObject {
|
|||
}
|
||||
|
||||
func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem, status: CIStatus? = nil) {
|
||||
if let ciIM = getCIItemsModel(cInfo, cItem),
|
||||
let i = getChatItemIndex(ciIM, cItem) {
|
||||
if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
|
||||
withConditionalAnimation {
|
||||
_updateChatItem(ciIM: ciIM, at: i, with: cItem)
|
||||
_updateChatItem(at: i, with: cItem)
|
||||
}
|
||||
} else if let status = status {
|
||||
chatItemStatuses.updateValue(status, forKey: cItem.id)
|
||||
}
|
||||
}
|
||||
|
||||
private func _updateChatItem(ciIM: ItemsModel, at i: Int, with cItem: ChatItem) {
|
||||
ciIM.reversedChatItems[i] = cItem
|
||||
ciIM.reversedChatItems[i].viewTimestamp = .now
|
||||
private func _updateChatItem(at i: Int, with cItem: ChatItem) {
|
||||
im.reversedChatItems[i] = cItem
|
||||
im.reversedChatItems[i].viewTimestamp = .now
|
||||
}
|
||||
|
||||
func getChatItemIndex(_ ciIM: ItemsModel, _ cItem: ChatItem) -> Int? {
|
||||
ciIM.reversedChatItems.firstIndex(where: { $0.id == cItem.id })
|
||||
func getChatItemIndex(_ cItem: ChatItem) -> Int? {
|
||||
im.reversedChatItems.firstIndex(where: { $0.id == cItem.id })
|
||||
}
|
||||
|
||||
func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
// 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()]
|
||||
}
|
||||
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 scope
|
||||
if let ciIM = getCIItemsModel(cInfo, cItem) {
|
||||
if let i = getChatItemIndex(ciIM, cItem) {
|
||||
// remove from current chat
|
||||
if chatId == cInfo.id {
|
||||
if let i = getChatItemIndex(cItem) {
|
||||
withAnimation {
|
||||
let item = ciIM.reversedChatItems.remove(at: i)
|
||||
ciIM.chatState.itemsRemoved([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed())
|
||||
let item = im.reversedChatItems.remove(at: i)
|
||||
im.chatState.itemsRemoved([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -741,7 +644,7 @@ final class ChatModel: ObservableObject {
|
|||
if chatId == groupInfo.id {
|
||||
for i in 0..<im.reversedChatItems.count {
|
||||
if let updatedItem = removedUpdatedItem(im.reversedChatItems[i]) {
|
||||
_updateChatItem(ciIM: im, at: i, with: updatedItem) // TODO [knocking] review: use getCIItemsModel?
|
||||
_updateChatItem(at: i, with: updatedItem)
|
||||
}
|
||||
}
|
||||
} else if let chat = getChat(groupInfo.id),
|
||||
|
@ -833,7 +736,7 @@ final class ChatModel: ObservableObject {
|
|||
im.reversedChatItems.first?.isLiveDummy == true
|
||||
}
|
||||
|
||||
func markAllChatItemsRead(_ chatIM: ItemsModel, _ cInfo: ChatInfo) {
|
||||
func markAllChatItemsRead(_ cInfo: ChatInfo) {
|
||||
// update preview
|
||||
_updateChat(cInfo.id) { chat in
|
||||
self.decreaseUnreadCounter(user: self.currentUser!, chat: chat)
|
||||
|
@ -844,7 +747,7 @@ final class ChatModel: ObservableObject {
|
|||
if chatId == cInfo.id {
|
||||
var i = 0
|
||||
while i < im.reversedChatItems.count {
|
||||
markChatItemRead_(chatIM, i)
|
||||
markChatItemRead_(i)
|
||||
i += 1
|
||||
}
|
||||
im.chatState.itemsRead(nil, im.reversedChatItems.reversed())
|
||||
|
@ -869,26 +772,27 @@ final class ChatModel: ObservableObject {
|
|||
}
|
||||
// clear current chat
|
||||
if chatId == cInfo.id {
|
||||
chatItemStatuses = [:]
|
||||
im.reversedChatItems = []
|
||||
im.chatState.clear()
|
||||
}
|
||||
}
|
||||
|
||||
func markChatItemsRead(_ chatIM: ItemsModel, _ cInfo: ChatInfo, _ itemIds: [ChatItem.ID], _ mentionsRead: Int) {
|
||||
func markChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID], _ mentionsRead: Int) {
|
||||
if self.chatId == cInfo.id {
|
||||
var unreadItemIds: Set<ChatItem.ID> = []
|
||||
var i = 0
|
||||
var ids = Set(itemIds)
|
||||
while i < chatIM.reversedChatItems.count && !ids.isEmpty {
|
||||
let item = chatIM.reversedChatItems[i]
|
||||
while i < im.reversedChatItems.count && !ids.isEmpty {
|
||||
let item = im.reversedChatItems[i]
|
||||
if ids.contains(item.id) && item.isRcvNew {
|
||||
markChatItemRead_(chatIM, i)
|
||||
markChatItemRead_(i)
|
||||
unreadItemIds.insert(item.id)
|
||||
ids.remove(item.id)
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
chatIM.chatState.itemsRead(unreadItemIds, chatIM.reversedChatItems.reversed())
|
||||
im.chatState.itemsRead(unreadItemIds, im.reversedChatItems.reversed())
|
||||
}
|
||||
self.unreadCollector.changeUnreadCounter(cInfo.id, by: -itemIds.count, unreadMentions: -mentionsRead)
|
||||
}
|
||||
|
@ -984,13 +888,13 @@ final class ChatModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func markChatItemRead_(_ chatIM: ItemsModel, _ i: Int) {
|
||||
let meta = chatIM.reversedChatItems[i].meta
|
||||
private func markChatItemRead_(_ i: Int) {
|
||||
let meta = im.reversedChatItems[i].meta
|
||||
if case .rcvNew = meta.itemStatus {
|
||||
chatIM.reversedChatItems[i].meta.itemStatus = .rcvRead
|
||||
chatIM.reversedChatItems[i].viewTimestamp = .now
|
||||
im.reversedChatItems[i].meta.itemStatus = .rcvRead
|
||||
im.reversedChatItems[i].viewTimestamp = .now
|
||||
if meta.itemLive != true, let ttl = meta.itemTimed?.ttl {
|
||||
chatIM.reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl)
|
||||
im.reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1069,7 +973,7 @@ final class ChatModel: ObservableObject {
|
|||
var count = 0
|
||||
var ns: [String] = []
|
||||
if let ciCategory = chatItem.mergeCategory,
|
||||
var i = getChatItemIndex(im, chatItem) { // TODO [knocking] review: use getCIItemsModel?
|
||||
var i = getChatItemIndex(chatItem) {
|
||||
while i < im.reversedChatItems.count {
|
||||
let ci = im.reversedChatItems[i]
|
||||
if ci.mergeCategory != ciCategory { break }
|
||||
|
@ -1085,7 +989,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(im, ci) { // TODO [knocking] review: use getCIItemsModel?
|
||||
if let i = getChatItemIndex(ci) {
|
||||
(i, i > 0 ? im.reversedChatItems[i - 1] : nil)
|
||||
} else {
|
||||
(nil, nil)
|
||||
|
@ -1196,7 +1100,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)
|
||||
}
|
||||
}
|
||||
|
@ -1248,34 +1152,6 @@ final class Chat: ObservableObject, Identifiable, ChatLike {
|
|||
)
|
||||
}
|
||||
|
||||
var userCanSend: Bool {
|
||||
switch chatInfo {
|
||||
case .direct: return true
|
||||
case let .group(groupInfo, groupChatScope):
|
||||
let m = groupInfo.membership
|
||||
return (m.memberActive && m.memberRole >= .member && !m.memberPending) || groupChatScope != nil
|
||||
case .local:
|
||||
return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
var userIsObserver: Bool {
|
||||
switch chatInfo {
|
||||
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
|
||||
|
@ -1288,18 +1164,6 @@ final class Chat: ObservableObject, Identifiable, ChatLike {
|
|||
|
||||
var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } }
|
||||
|
||||
var supportUnreadCount: Int {
|
||||
switch chatInfo {
|
||||
case let .group(groupInfo, _):
|
||||
if groupInfo.canModerate {
|
||||
return groupInfo.membersRequireAttention
|
||||
} else {
|
||||
return groupInfo.membership.supportChat?.unread ?? 0
|
||||
}
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
|
||||
public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
||||
}
|
||||
|
||||
|
|
|
@ -344,54 +344,43 @@ func apiGetChatTagsAsync() async throws -> [ChatTag] {
|
|||
|
||||
let loadItemsPerPage = 50
|
||||
|
||||
func apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag? = nil, pagination: ChatPagination, search: String = "") async throws -> (Chat, NavigationInfo) {
|
||||
let r: ChatResponse0 = try await chatSendCmd(.apiGetChat(chatId: chatId, scope: scope, contentTag: contentTag, pagination: pagination, search: search))
|
||||
func apiGetChat(chatId: ChatId, pagination: ChatPagination, search: String = "") async throws -> (Chat, NavigationInfo) {
|
||||
let r: ChatResponse0 = try await chatSendCmd(.apiGetChat(chatId: chatId, pagination: pagination, search: search))
|
||||
if case let .apiChat(_, chat, navInfo) = r { return (Chat.init(chat), navInfo ?? NavigationInfo()) }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
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(chat: Chat, search: String = "", clearItems: Bool = true) async {
|
||||
await loadChat(chatId: chat.chatInfo.id, search: search, clearItems: clearItems)
|
||||
}
|
||||
|
||||
func loadChat(chatId: ChatId, im: ItemsModel, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async {
|
||||
func loadChat(chatId: ChatId, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async {
|
||||
let m = ChatModel.shared
|
||||
let im = ItemsModel.shared
|
||||
await MainActor.run {
|
||||
m.chatItemStatuses = [:]
|
||||
if clearItems {
|
||||
im.reversedChatItems = []
|
||||
im.chatState.clear()
|
||||
ItemsModel.shared.chatState.clear()
|
||||
}
|
||||
}
|
||||
await apiLoadMessages(
|
||||
chatId,
|
||||
im,
|
||||
( // pagination
|
||||
openAroundItemId != nil
|
||||
? .around(chatItemId: openAroundItemId!, count: loadItemsPerPage)
|
||||
: (
|
||||
search == ""
|
||||
? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage)
|
||||
)
|
||||
),
|
||||
search,
|
||||
openAroundItemId,
|
||||
{ 0...0 }
|
||||
)
|
||||
await apiLoadMessages(chatId, openAroundItemId != nil ? .around(chatItemId: openAroundItemId!, count: loadItemsPerPage) : (search == "" ? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage)), im.chatState, search, openAroundItemId, { 0...0 })
|
||||
}
|
||||
|
||||
func apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64) async throws -> ChatItemInfo {
|
||||
let r: ChatResponse0 = try await chatSendCmd(.apiGetChatItemInfo(type: type, id: id, scope: scope, itemId: itemId))
|
||||
func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -> ChatItemInfo {
|
||||
let r: ChatResponse0 = try await chatSendCmd(.apiGetChatItemInfo(type: type, id: id, itemId: itemId))
|
||||
if case let .chatItemInfo(_, _, chatItemInfo) = r { return chatItemInfo }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiPlanForwardChatItems(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64]) async throws -> ([Int64], ForwardConfirmation?) {
|
||||
let r: ChatResponse1 = try await chatSendCmd(.apiPlanForwardChatItems(fromChatType: type, fromChatId: id, fromScope: scope, itemIds: itemIds))
|
||||
func apiPlanForwardChatItems(type: ChatType, id: Int64, itemIds: [Int64]) async throws -> ([Int64], ForwardConfirmation?) {
|
||||
let r: ChatResponse1 = try await chatSendCmd(.apiPlanForwardChatItems(toChatType: type, toChatId: id, itemIds: itemIds))
|
||||
if case let .forwardPlan(_, chatItemIds, forwardConfimation) = r { return (chatItemIds, forwardConfimation) }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
return await processSendMessageCmd(toChatType: toChatType, cmd: cmd)
|
||||
}
|
||||
|
||||
|
@ -423,8 +412,8 @@ func apiReorderChatTags(tagIds: [Int64]) async throws {
|
|||
try await sendCommandOkResp(.apiReorderChatTags(tagIds: tagIds))
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
return await processSendMessageCmd(toChatType: type, cmd: cmd)
|
||||
}
|
||||
|
||||
|
@ -501,8 +490,8 @@ private func createChatItemsErrorAlert(_ r: ChatError) {
|
|||
)
|
||||
}
|
||||
|
||||
func apiUpdateChatItem(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool = false) async throws -> ChatItem {
|
||||
let r: ChatResponse1 = try await chatSendCmd(.apiUpdateChatItem(type: type, id: id, scope: scope, itemId: itemId, updatedMessage: updatedMessage, live: live), bgDelay: msgDelay)
|
||||
func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool = false) async throws -> ChatItem {
|
||||
let r: ChatResponse1 = try await chatSendCmd(.apiUpdateChatItem(type: type, id: id, itemId: itemId, updatedMessage: updatedMessage, live: live), bgDelay: msgDelay)
|
||||
switch r {
|
||||
case let .chatItemUpdated(_, aChatItem): return aChatItem.chatItem
|
||||
case let .chatItemNotChanged(_, aChatItem): return aChatItem.chatItem
|
||||
|
@ -510,8 +499,8 @@ func apiUpdateChatItem(type: ChatType, id: Int64, scope: GroupChatScope?, itemId
|
|||
}
|
||||
}
|
||||
|
||||
func apiChatItemReaction(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64, add: Bool, reaction: MsgReaction) async throws -> ChatItem {
|
||||
let r: ChatResponse1 = try await chatSendCmd(.apiChatItemReaction(type: type, id: id, scope: scope, itemId: itemId, add: add, reaction: reaction), bgDelay: msgDelay)
|
||||
func apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction) async throws -> ChatItem {
|
||||
let r: ChatResponse1 = try await chatSendCmd(.apiChatItemReaction(type: type, id: id, itemId: itemId, add: add, reaction: reaction), bgDelay: msgDelay)
|
||||
if case let .chatItemReaction(_, _, reaction) = r { return reaction.chatReaction.chatItem }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
@ -523,8 +512,8 @@ func apiGetReactionMembers(groupId: Int64, itemId: Int64, reaction: MsgReaction)
|
|||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiDeleteChatItems(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] {
|
||||
let r: ChatResponse1 = try await chatSendCmd(.apiDeleteChatItem(type: type, id: id, scope: scope, itemIds: itemIds, mode: mode), bgDelay: msgDelay)
|
||||
func apiDeleteChatItems(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] {
|
||||
let r: ChatResponse1 = try await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemIds: itemIds, mode: mode), bgDelay: msgDelay)
|
||||
if case let .chatItemsDeleted(_, items, _) = r { return items }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
@ -1248,14 +1237,12 @@ func apiRejectContactRequest(contactReqId: Int64) async throws {
|
|||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiChatRead(type: ChatType, id: Int64, scope: GroupChatScope?) async throws {
|
||||
try await sendCommandOkResp(.apiChatRead(type: type, id: id, scope: scope))
|
||||
func apiChatRead(type: ChatType, id: Int64) async throws {
|
||||
try await sendCommandOkResp(.apiChatRead(type: type, id: id))
|
||||
}
|
||||
|
||||
func apiChatItemsRead(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64]) async throws -> ChatInfo {
|
||||
let r: ChatResponse1 = try await chatSendCmd(.apiChatItemsRead(type: type, id: id, scope: scope, itemIds: itemIds))
|
||||
if case let .itemsReadForChat(_, updatedChatInfo) = r { return updatedChatInfo }
|
||||
throw r.unexpected
|
||||
func apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) async throws {
|
||||
try await sendCommandOkResp(.apiChatItemsRead(type: type, id: id, itemIds: itemIds))
|
||||
}
|
||||
|
||||
func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
|
||||
|
@ -1558,13 +1545,13 @@ func apiGetNetworkStatuses() throws -> [ConnNetworkStatus] {
|
|||
throw r.unexpected
|
||||
}
|
||||
|
||||
func markChatRead(_ im: ItemsModel, _ chat: Chat) async {
|
||||
func markChatRead(_ chat: Chat) async {
|
||||
do {
|
||||
if chat.chatStats.unreadCount > 0 {
|
||||
let cInfo = chat.chatInfo
|
||||
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, scope: cInfo.groupChatScope())
|
||||
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId)
|
||||
await MainActor.run {
|
||||
withAnimation { ChatModel.shared.markAllChatItemsRead(im, cInfo) }
|
||||
withAnimation { ChatModel.shared.markAllChatItemsRead(cInfo) }
|
||||
}
|
||||
}
|
||||
if chat.chatStats.unreadChat {
|
||||
|
@ -1587,12 +1574,11 @@ func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async {
|
|||
}
|
||||
}
|
||||
|
||||
func apiMarkChatItemsRead(_ im: ItemsModel, _ cInfo: ChatInfo, _ itemIds: [ChatItem.ID], mentionsRead: Int) async {
|
||||
func apiMarkChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID], mentionsRead: Int) async {
|
||||
do {
|
||||
let updatedChatInfo = try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, scope: cInfo.groupChatScope(), itemIds: itemIds)
|
||||
await MainActor.run {
|
||||
ChatModel.shared.updateChatInfo(updatedChatInfo)
|
||||
ChatModel.shared.markChatItemsRead(im, cInfo, itemIds, mentionsRead)
|
||||
try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, itemIds: itemIds)
|
||||
DispatchQueue.main.async {
|
||||
ChatModel.shared.markChatItemsRead(cInfo, itemIds, mentionsRead)
|
||||
}
|
||||
} catch {
|
||||
logger.error("apiChatItemsRead error: \(responseError(error))")
|
||||
|
@ -1640,15 +1626,9 @@ func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult {
|
|||
}
|
||||
}
|
||||
|
||||
func apiAcceptMember(_ groupId: Int64, _ groupMemberId: Int64, _ memberRole: GroupMemberRole) async throws -> (GroupInfo, GroupMember) {
|
||||
let r: ChatResponse2 = try await chatSendCmd(.apiAcceptMember(groupId: groupId, groupMemberId: groupMemberId, memberRole: memberRole))
|
||||
if case let .memberAccepted(_, groupInfo, member) = r { return (groupInfo, member) }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiRemoveMembers(_ groupId: Int64, _ memberIds: [Int64], _ withMessages: Bool = false) async throws -> (GroupInfo, [GroupMember]) {
|
||||
func apiRemoveMembers(_ groupId: Int64, _ memberIds: [Int64], _ withMessages: Bool = false) async throws -> [GroupMember] {
|
||||
let r: ChatResponse2 = try await chatSendCmd(.apiRemoveMembers(groupId: groupId, memberIds: memberIds, withMessages: withMessages), bgTask: false)
|
||||
if case let .userDeletedMembers(_, updatedGroupInfo, members, _withMessages) = r { return (updatedGroupInfo, members) }
|
||||
if case let .userDeletedMembers(_, _, members, withMessages) = r { return members }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
|
@ -2152,7 +2132,7 @@ func processReceivedMsg(_ res: ChatEvent) async {
|
|||
let cInfo = chatItem.chatInfo
|
||||
let cItem = chatItem.chatItem
|
||||
if !cItem.isDeletedContent && active(user) {
|
||||
_ = await MainActor.run { m.upsertChatItem(cInfo, cItem) }
|
||||
await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) }
|
||||
}
|
||||
if let endTask = m.messageDelivery[cItem.id] {
|
||||
switch cItem.meta.itemStatus {
|
||||
|
@ -2248,13 +2228,6 @@ func processReceivedMsg(_ res: ChatEvent) async {
|
|||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
}
|
||||
case let .memberAcceptedByOther(user, groupInfo, _, member):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
m.updateGroup(groupInfo)
|
||||
}
|
||||
}
|
||||
case let .deletedMemberUser(user, groupInfo, member, withMessages): // TODO update user member
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
|
@ -2267,7 +2240,6 @@ func processReceivedMsg(_ res: ChatEvent) async {
|
|||
case let .deletedMember(user, groupInfo, byMember, deletedMember, withMessages):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateGroup(groupInfo)
|
||||
_ = m.upsertGroupMember(groupInfo, deletedMember)
|
||||
if withMessages {
|
||||
m.removeMemberItems(deletedMember, byMember: byMember, groupInfo)
|
||||
|
@ -2277,7 +2249,6 @@ func processReceivedMsg(_ res: ChatEvent) async {
|
|||
case let .leftMember(user, groupInfo, member):
|
||||
if active(user) {
|
||||
await MainActor.run {
|
||||
m.updateGroup(groupInfo)
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
}
|
||||
|
@ -2292,12 +2263,6 @@ func processReceivedMsg(_ res: ChatEvent) async {
|
|||
await MainActor.run {
|
||||
m.updateGroup(groupInfo)
|
||||
}
|
||||
if m.chatId == groupInfo.id,
|
||||
case .memberSupport(nil) = m.secondaryIM?.groupScopeInfo {
|
||||
await MainActor.run {
|
||||
m.secondaryPendingInviteeChatOpened = false
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .joinedGroupMember(user, groupInfo, member):
|
||||
if active(user) {
|
||||
|
@ -2584,7 +2549,7 @@ func groupChatItemsDeleted(_ user: UserRef, _ groupInfo: GroupInfo, _ chatItemID
|
|||
return
|
||||
}
|
||||
let im = ItemsModel.shared
|
||||
let cInfo = ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil)
|
||||
let cInfo = ChatInfo.group(groupInfo: groupInfo)
|
||||
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, im: ItemsModel.shared, clearItems: false) }
|
||||
Task { await loadChat(chat: chat, clearItems: false) }
|
||||
}
|
||||
if let ncr = chatModel.ntfContactRequest {
|
||||
await MainActor.run { chatModel.ntfContactRequest = nil }
|
||||
|
|
|
@ -22,28 +22,11 @@ struct ChatInfoToolbar: View {
|
|||
Image(systemName: "theatermasks").frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.indigo)
|
||||
Spacer().frame(width: 16)
|
||||
}
|
||||
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.supportUnreadCount > 0 {
|
||||
Image(systemName: "flag.circle.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 14, height: 14)
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(.white, theme.colors.primary)
|
||||
}
|
||||
}
|
||||
ChatInfoImage(
|
||||
chat: chat,
|
||||
size: imageSize,
|
||||
color: Color(uiColor: .tertiaryLabel)
|
||||
)
|
||||
.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, im: ItemsModel.shared, clearItems: true)
|
||||
await loadChat(chat: chat, 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, im: ItemsModel.shared, clearItems: true)
|
||||
await loadChat(chat: chat, 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, groupChatScope: nil))
|
||||
chat.wrappedValue = Chat.init(chatInfo: ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil))
|
||||
ChatModel.shared.updateChatInfo(ChatInfo.group(groupInfo: groupInfo))
|
||||
chat.wrappedValue = Chat.init(chatInfo: ChatInfo.group(groupInfo: groupInfo))
|
||||
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(im, chatItem) {
|
||||
if var i = m.getChatItemIndex(chatItem) {
|
||||
while i < im.reversedChatItems.count,
|
||||
let f = featureInfo(im.reversedChatItems[i]) {
|
||||
if !icons.contains(f.icon) {
|
||||
|
@ -108,7 +108,6 @@ 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,7 +278,6 @@ 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),
|
||||
|
@ -294,16 +293,16 @@ struct CIFileView_Previews: PreviewProvider {
|
|||
file: nil
|
||||
)
|
||||
Group {
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: sentFile, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
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), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: fileChatItemWtFile, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
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 })
|
||||
}
|
||||
.environment(\.revealed, false)
|
||||
.previewLayout(.fixed(width: 360, height: 360))
|
||||
|
|
|
@ -12,7 +12,7 @@ import SimpleXChat
|
|||
struct CIImageView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
let chatItem: ChatItem
|
||||
var scrollToItem: ((ChatItem.ID) -> Void)? = nil
|
||||
var scrollToItemId: ((ChatItem.ID) -> Void)? = nil
|
||||
var preview: UIImage?
|
||||
let maxWidth: CGFloat
|
||||
var imgWidth: CGFloat?
|
||||
|
@ -26,7 +26,7 @@ struct CIImageView: View {
|
|||
if let uiImage = getLoadedImage(file) {
|
||||
Group { if smallView { smallViewImageView(uiImage) } else { imageView(uiImage) } }
|
||||
.fullScreenCover(isPresented: $showFullScreenImage) {
|
||||
FullScreenMediaView(chatItem: chatItem, scrollToItem: scrollToItem, image: uiImage, showView: $showFullScreenImage)
|
||||
FullScreenMediaView(chatItem: chatItem, scrollToItemId: scrollToItemId, image: uiImage, showView: $showFullScreenImage)
|
||||
}
|
||||
.if(!smallView) { view in
|
||||
view.modifier(PrivacyBlur(blurred: $blurred))
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -435,7 +435,6 @@ 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),
|
||||
|
@ -458,10 +457,10 @@ struct CIVoiceView_Previews: PreviewProvider {
|
|||
duration: 30,
|
||||
allowMenu: Binding.constant(true)
|
||||
)
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: sentVoiceMessage, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: .constant(true))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: .constant(true))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: .constant(true))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: voiceMessageWtFile, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: .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))
|
||||
}
|
||||
.previewLayout(.fixed(width: 360, height: 360))
|
||||
}
|
||||
|
|
|
@ -77,7 +77,6 @@ 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),
|
||||
|
@ -93,11 +92,11 @@ struct FramedCIVoiceView_Previews: PreviewProvider {
|
|||
file: CIFile.getSample(fileStatus: .sndComplete)
|
||||
)
|
||||
Group {
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: sentVoiceMessage, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
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."), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: voiceMessageWithQuote, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
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 })
|
||||
}
|
||||
.environment(\.revealed, false)
|
||||
.previewLayout(.fixed(width: 360, height: 360))
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -14,7 +14,7 @@ import AVKit
|
|||
struct FullScreenMediaView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@State var chatItem: ChatItem
|
||||
var scrollToItem: ((ChatItem.ID) -> Void)?
|
||||
var scrollToItemId: ((ChatItem.ID) -> Void)?
|
||||
@State var image: UIImage?
|
||||
@State var player: AVPlayer? = nil
|
||||
@State var url: URL? = nil
|
||||
|
@ -71,7 +71,7 @@ struct FullScreenMediaView: View {
|
|||
let w = abs(t.width)
|
||||
if t.height > 60 && t.height > w * 2 {
|
||||
showView = false
|
||||
scrollToItem?(chatItem.id)
|
||||
scrollToItemId?(chatItem.id)
|
||||
} else if w > 60 && w > abs(t.height) * 2 && !scrolling {
|
||||
let previous = t.width > 0
|
||||
scrolling = true
|
||||
|
|
|
@ -14,7 +14,6 @@ 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 {
|
||||
|
@ -30,14 +29,14 @@ struct MarkedDeletedItemView: View {
|
|||
var mergedMarkedDeletedText: LocalizedStringKey {
|
||||
if !revealed,
|
||||
let ciCategory = chatItem.mergeCategory,
|
||||
var i = m.getChatItemIndex(im, chatItem) {
|
||||
var i = m.getChatItemIndex(chatItem) {
|
||||
var moderated = 0
|
||||
var blocked = 0
|
||||
var blockedByAdmin = 0
|
||||
var deleted = 0
|
||||
var moderatedBy: Set<String> = []
|
||||
while i < im.reversedChatItems.count,
|
||||
let ci = .some(im.reversedChatItems[i]),
|
||||
while i < ItemsModel.shared.reversedChatItems.count,
|
||||
let ci = .some(ItemsModel.shared.reversedChatItems[i]),
|
||||
ci.mergeCategory == ciCategory,
|
||||
let itemDeleted = ci.meta.itemDeleted {
|
||||
switch itemDeleted {
|
||||
|
@ -86,7 +85,6 @@ 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)
|
||||
}
|
||||
|
|
|
@ -40,31 +40,25 @@ 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
|
||||
var chatItem: ChatItem
|
||||
var scrollToItem: (ChatItem.ID) -> Void
|
||||
@Binding var scrollToItemId: ChatItem.ID?
|
||||
var scrollToItemId: (ChatItem.ID) -> Void
|
||||
var maxWidth: CGFloat = .infinity
|
||||
@Binding var allowMenu: Bool
|
||||
|
||||
init(
|
||||
chat: Chat,
|
||||
im: ItemsModel,
|
||||
chatItem: ChatItem,
|
||||
scrollToItem: @escaping (ChatItem.ID) -> Void,
|
||||
scrollToItemId: Binding<ChatItem.ID?> = .constant(nil),
|
||||
scrollToItemId: @escaping (ChatItem.ID) -> Void,
|
||||
showMember: Bool = false,
|
||||
maxWidth: CGFloat = .infinity,
|
||||
allowMenu: Binding<Bool> = .constant(false)
|
||||
) {
|
||||
self.chat = chat
|
||||
self.im = im
|
||||
self.chatItem = chatItem
|
||||
self.scrollToItem = scrollToItem
|
||||
_scrollToItemId = scrollToItemId
|
||||
self.scrollToItemId = scrollToItemId
|
||||
self.maxWidth = maxWidth
|
||||
_allowMenu = allowMenu
|
||||
}
|
||||
|
@ -72,14 +66,14 @@ struct ChatItemView: View {
|
|||
var body: some View {
|
||||
let ci = chatItem
|
||||
if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) {
|
||||
MarkedDeletedItemView(chat: chat, im: im, chatItem: chatItem)
|
||||
MarkedDeletedItemView(chat: chat, 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, im: im, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
|
||||
ChatItemContentView(chat: chat, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
|
||||
} else {
|
||||
framedItemView()
|
||||
}
|
||||
|
@ -107,10 +101,8 @@ struct ChatItemView: View {
|
|||
}()
|
||||
return FramedItemView(
|
||||
chat: chat,
|
||||
im: im,
|
||||
chatItem: chatItem,
|
||||
scrollToItem: scrollToItem,
|
||||
scrollToItemId: $scrollToItemId,
|
||||
scrollToItemId: scrollToItemId,
|
||||
preview: preview,
|
||||
maxWidth: maxWidth,
|
||||
imgWidth: adjustedMaxWidth,
|
||||
|
@ -125,7 +117,6 @@ 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
|
||||
|
@ -149,9 +140,7 @@ 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()
|
||||
|
@ -160,7 +149,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, im: im, chatItem: chatItem, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary)
|
||||
CIChatFeatureView(chat: chat, 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)
|
||||
|
@ -192,13 +181,6 @@ 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)
|
||||
|
@ -214,7 +196,7 @@ struct ChatItemContentView<Content: View>: View {
|
|||
}
|
||||
|
||||
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
|
||||
CIChatFeatureView(chat: chat, im: im, chatItem: chatItem, feature: feature, iconColor: iconColor)
|
||||
CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, iconColor: iconColor)
|
||||
}
|
||||
|
||||
private var mergedGroupEventText: Text? {
|
||||
|
@ -274,17 +256,16 @@ 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, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getDeletedContentSample(), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)).environment(\.revealed, true)
|
||||
ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)).environment(\.revealed, true)
|
||||
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)
|
||||
}
|
||||
.environment(\.revealed, false)
|
||||
.previewLayout(.fixed(width: 360, height: 70))
|
||||
|
@ -294,12 +275,10 @@ 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)),
|
||||
|
@ -307,12 +286,10 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
|||
quotedItem: nil,
|
||||
file: nil
|
||||
),
|
||||
scrollToItem: { _ in },
|
||||
scrollToItemId: Binding.constant(nil)
|
||||
scrollToItemId: { _ in }
|
||||
)
|
||||
ChatItemView(
|
||||
chat: Chat.sampleData,
|
||||
im: im,
|
||||
chatItem: ChatItem(
|
||||
chatDir: .directRcv,
|
||||
meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead),
|
||||
|
@ -320,11 +297,10 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
|||
quotedItem: nil,
|
||||
file: nil
|
||||
),
|
||||
scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)
|
||||
scrollToItemId: { _ in }
|
||||
)
|
||||
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)),
|
||||
|
@ -332,12 +308,10 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
|||
quotedItem: nil,
|
||||
file: nil
|
||||
),
|
||||
scrollToItem: { _ in },
|
||||
scrollToItemId: Binding.constant(nil)
|
||||
scrollToItemId: { _ in }
|
||||
)
|
||||
ChatItemView(
|
||||
chat: Chat.sampleData,
|
||||
im: im,
|
||||
chatItem: ChatItem(
|
||||
chatDir: .directRcv,
|
||||
meta: CIMeta.getSample(1, .now, "group event text", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
|
||||
|
@ -345,12 +319,10 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
|||
quotedItem: nil,
|
||||
file: nil
|
||||
),
|
||||
scrollToItem: { _ in },
|
||||
scrollToItemId: Binding.constant(nil)
|
||||
scrollToItemId: { _ in }
|
||||
)
|
||||
ChatItemView(
|
||||
chat: Chat.sampleData,
|
||||
im: im,
|
||||
chatItem: ChatItem(
|
||||
chatDir: .directRcv,
|
||||
meta: CIMeta.getSample(1, .now, ciFeatureContent.text, .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
|
||||
|
@ -358,8 +330,7 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
|
|||
quotedItem: nil,
|
||||
file: nil
|
||||
),
|
||||
scrollToItem: { _ in },
|
||||
scrollToItemId: Binding.constant(nil)
|
||||
scrollToItemId: { _ in }
|
||||
)
|
||||
}
|
||||
.environment(\.revealed, true)
|
||||
|
|
|
@ -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, scope: im.groupScopeInfo?.toChatScope(), contentTag: im.contentTag, pagination: pagination, search: search)
|
||||
(chat, navInfo) = try await apiGetChat(chatId: chatId, pagination: pagination, search: search)
|
||||
} catch let error {
|
||||
logger.error("apiLoadMessages error: \(responseError(error))")
|
||||
return
|
||||
|
@ -38,31 +38,30 @@ func apiLoadMessages(
|
|||
return
|
||||
}
|
||||
|
||||
let unreadAfterItemId = im.chatState.unreadAfterItemId
|
||||
let unreadAfterItemId = chatState.unreadAfterItemId
|
||||
|
||||
let oldItems = Array(im.reversedChatItems.reversed())
|
||||
let oldItems = Array(ItemsModel.shared.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 im.secondaryIMFilter == nil && chatModel.getChat(chat.id) == nil {
|
||||
if chatModel.getChat(chat.id) == nil {
|
||||
chatModel.addChat(chat)
|
||||
}
|
||||
await MainActor.run {
|
||||
im.reversedChatItems = chat.chatItems.reversed()
|
||||
if im.secondaryIMFilter == nil {
|
||||
chatModel.updateChatInfo(chat.chatInfo)
|
||||
}
|
||||
im.chatState.splits = newSplits
|
||||
chatModel.chatItemStatuses.removeAll()
|
||||
ItemsModel.shared.reversedChatItems = chat.chatItems.reversed()
|
||||
chatModel.updateChatInfo(chat.chatInfo)
|
||||
chatState.splits = newSplits
|
||||
if !chat.chatItems.isEmpty {
|
||||
im.chatState.unreadAfterItemId = chat.chatItems.last!.id
|
||||
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
|
||||
chatState.totalAfter = navInfo.afterTotal
|
||||
chatState.unreadTotal = chat.chatStats.unreadCount
|
||||
chatState.unreadAfter = navInfo.afterUnread
|
||||
chatState.unreadAfterNewestLoaded = navInfo.afterUnread
|
||||
|
||||
im.preloadState.clear()
|
||||
PreloadState.shared.clear()
|
||||
}
|
||||
case let .before(paginationChatItemId, _):
|
||||
newItems.append(contentsOf: oldItems)
|
||||
|
@ -72,15 +71,15 @@ func apiLoadMessages(
|
|||
let wasSize = newItems.count
|
||||
let visibleItemIndexes = await MainActor.run { visibleItemIndexesNonReversed() }
|
||||
let modifiedSplits = removeDuplicatesAndModifySplitsOnBeforePagination(
|
||||
unreadAfterItemId, &newItems, newIds, im.chatState.splits, visibleItemIndexes
|
||||
unreadAfterItemId, &newItems, newIds, 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 {
|
||||
im.reversedChatItems = newReversed
|
||||
im.chatState.splits = modifiedSplits.newSplits
|
||||
im.chatState.moveUnreadAfterItem(modifiedSplits.oldUnreadSplitIndex, modifiedSplits.newUnreadSplitIndex, oldItems)
|
||||
ItemsModel.shared.reversedChatItems = newReversed
|
||||
chatState.splits = modifiedSplits.newSplits
|
||||
chatState.moveUnreadAfterItem(modifiedSplits.oldUnreadSplitIndex, modifiedSplits.newUnreadSplitIndex, oldItems)
|
||||
}
|
||||
case let .after(paginationChatItemId, _):
|
||||
newItems.append(contentsOf: oldItems)
|
||||
|
@ -90,7 +89,7 @@ func apiLoadMessages(
|
|||
let mappedItems = mapItemsToIds(chat.chatItems)
|
||||
let newIds = mappedItems.0
|
||||
let (newSplits, unreadInLoaded) = removeDuplicatesAndModifySplitsOnAfterPagination(
|
||||
mappedItems.1, paginationChatItemId, &newItems, newIds, chat, im.chatState.splits
|
||||
mappedItems.1, paginationChatItemId, &newItems, newIds, chat, chatState.splits
|
||||
)
|
||||
let indexToAdd = min(indexInCurrentItems + 1, newItems.count)
|
||||
let indexToAddIsLast = indexToAdd == newItems.count
|
||||
|
@ -98,19 +97,19 @@ func apiLoadMessages(
|
|||
let new: [ChatItem] = newItems
|
||||
let newReversed: [ChatItem] = newItems.reversed()
|
||||
await MainActor.run {
|
||||
im.reversedChatItems = newReversed
|
||||
im.chatState.splits = newSplits
|
||||
im.chatState.moveUnreadAfterItem(im.chatState.splits.first ?? new.last!.id, new)
|
||||
ItemsModel.shared.reversedChatItems = newReversed
|
||||
chatState.splits = newSplits
|
||||
chatState.moveUnreadAfterItem(chatState.splits.first ?? new.last!.id, new)
|
||||
// loading clear bottom area, updating number of unread items after the newest loaded item
|
||||
if indexToAddIsLast {
|
||||
im.chatState.unreadAfterNewestLoaded -= unreadInLoaded
|
||||
chatState.unreadAfterNewestLoaded -= unreadInLoaded
|
||||
}
|
||||
}
|
||||
case .around:
|
||||
var newSplits: [Int64]
|
||||
if openAroundItemId == nil {
|
||||
newItems.append(contentsOf: oldItems)
|
||||
newSplits = await removeDuplicatesAndUpperSplits(&newItems, chat, im.chatState.splits, visibleItemIndexesNonReversed)
|
||||
newSplits = await removeDuplicatesAndUpperSplits(&newItems, chat, chatState.splits, visibleItemIndexesNonReversed)
|
||||
} else {
|
||||
newSplits = []
|
||||
}
|
||||
|
@ -121,37 +120,33 @@ func apiLoadMessages(
|
|||
let newReversed: [ChatItem] = newItems.reversed()
|
||||
let orderedSplits = newSplits
|
||||
await MainActor.run {
|
||||
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
|
||||
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
|
||||
|
||||
if let openAroundItemId {
|
||||
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
|
||||
}
|
||||
chatState.unreadAfterNewestLoaded = navInfo.afterUnread
|
||||
ChatModel.shared.openAroundItemId = openAroundItemId
|
||||
ChatModel.shared.chatId = chatId
|
||||
} else {
|
||||
// no need to set it, count will be wrong
|
||||
// chatState.unreadAfterNewestLoaded = navInfo.afterUnread
|
||||
}
|
||||
im.preloadState.clear()
|
||||
PreloadState.shared.clear()
|
||||
}
|
||||
case .last:
|
||||
newItems.append(contentsOf: oldItems)
|
||||
let newSplits = await removeDuplicatesAndUnusedSplits(&newItems, chat, im.chatState.splits)
|
||||
let newSplits = await removeDuplicatesAndUnusedSplits(&newItems, chat, chatState.splits)
|
||||
newItems.append(contentsOf: chat.chatItems)
|
||||
let items = newItems
|
||||
await MainActor.run {
|
||||
im.reversedChatItems = items.reversed()
|
||||
im.chatState.splits = newSplits
|
||||
if im.secondaryIMFilter == nil {
|
||||
chatModel.updateChatInfo(chat.chatInfo)
|
||||
}
|
||||
im.chatState.unreadAfterNewestLoaded = 0
|
||||
ItemsModel.shared.reversedChatItems = items.reversed()
|
||||
chatState.splits = newSplits
|
||||
chatModel.updateChatInfo(chat.chatInfo)
|
||||
chatState.unreadAfterNewestLoaded = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import SwiftUI
|
|||
import SimpleXChat
|
||||
|
||||
struct MergedItems: Hashable, Equatable {
|
||||
let im: ItemsModel
|
||||
let items: [MergedItem]
|
||||
let splits: [SplitRange]
|
||||
// chat item id, index in list
|
||||
|
@ -24,15 +23,15 @@ struct MergedItems: Hashable, Equatable {
|
|||
hasher.combine("\(items.hashValue)")
|
||||
}
|
||||
|
||||
static func create(_ im: ItemsModel, _ revealedItems: Set<Int64>) -> MergedItems {
|
||||
if im.reversedChatItems.isEmpty {
|
||||
return MergedItems(im: im, items: [], splits: [], indexInParentItems: [:])
|
||||
static func create(_ items: [ChatItem], _ revealedItems: Set<Int64>, _ chatState: ActiveChatState) -> MergedItems {
|
||||
if items.isEmpty {
|
||||
return MergedItems(items: [], splits: [], indexInParentItems: [:])
|
||||
}
|
||||
|
||||
let unreadCount = im.chatState.unreadTotal
|
||||
let unreadCount = chatState.unreadTotal
|
||||
|
||||
let unreadAfterItemId = im.chatState.unreadAfterItemId
|
||||
let itemSplits = im.chatState.splits
|
||||
let unreadAfterItemId = chatState.unreadAfterItemId
|
||||
let itemSplits = chatState.splits
|
||||
var mergedItems: [MergedItem] = []
|
||||
// Indexes of splits here will be related to reversedChatItems, not chatModel.chatItems
|
||||
var splitRanges: [SplitRange] = []
|
||||
|
@ -41,19 +40,19 @@ struct MergedItems: Hashable, Equatable {
|
|||
var unclosedSplitIndex: Int? = nil
|
||||
var unclosedSplitIndexInParent: Int? = nil
|
||||
var visibleItemIndexInParent = -1
|
||||
var unreadBefore = unreadCount - im.chatState.unreadAfterNewestLoaded
|
||||
var unreadBefore = unreadCount - chatState.unreadAfterNewestLoaded
|
||||
var lastRevealedIdsInMergedItems: BoxedValue<[Int64]>? = nil
|
||||
var lastRangeInReversedForMergedItems: BoxedValue<ClosedRange<Int>>? = nil
|
||||
var recent: MergedItem? = 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
|
||||
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
|
||||
let category = item.mergeCategory
|
||||
let itemIsSplit = itemSplits.contains(item.id)
|
||||
|
||||
if item.id == unreadAfterItemId {
|
||||
unreadBefore = unreadCount - im.chatState.unreadAfter
|
||||
unreadBefore = unreadCount - chatState.unreadAfter
|
||||
}
|
||||
if item.isRcvNew {
|
||||
unreadBefore -= 1
|
||||
|
@ -107,19 +106,18 @@ 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: im.reversedChatItems[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index - 1, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent - 1))
|
||||
splitRanges.append(SplitRange(itemId: items[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index - 1, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent - 1))
|
||||
}
|
||||
unclosedSplitIndex = index
|
||||
unclosedSplitIndexInParent = visibleItemIndexInParent
|
||||
} else if index + 1 == im.reversedChatItems.count, let unclosedSplitIndex, let unclosedSplitIndexInParent {
|
||||
} else if index + 1 == items.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: im.reversedChatItems[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent))
|
||||
splitRanges.append(SplitRange(itemId: items[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent))
|
||||
}
|
||||
indexInParentItems[item.id] = visibleItemIndexInParent
|
||||
index += 1
|
||||
}
|
||||
return MergedItems(
|
||||
im: im,
|
||||
items: mergedItems,
|
||||
splits: splitRanges,
|
||||
indexInParentItems: indexInParentItems
|
||||
|
@ -129,6 +127,7 @@ 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
|
||||
|
@ -435,7 +434,7 @@ class BoxedValue<T: Hashable>: Equatable, Hashable {
|
|||
}
|
||||
|
||||
@MainActor
|
||||
func visibleItemIndexesNonReversed(_ im: ItemsModel, _ listState: EndlessScrollView<MergedItem>.ListState, _ mergedItems: MergedItems) -> ClosedRange<Int> {
|
||||
func visibleItemIndexesNonReversed(_ listState: EndlessScrollView<MergedItem>.ListState, _ mergedItems: MergedItems) -> ClosedRange<Int> {
|
||||
let zero = 0 ... 0
|
||||
let items = mergedItems.items
|
||||
if items.isEmpty {
|
||||
|
@ -446,12 +445,12 @@ func visibleItemIndexesNonReversed(_ im: ItemsModel, _ listState: EndlessScrollV
|
|||
guard let newest, let oldest else {
|
||||
return zero
|
||||
}
|
||||
let size = im.reversedChatItems.count
|
||||
let size = ItemsModel.shared.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 im.reversedChatItems.reversed()
|
||||
// visible items mapped to their underlying data structure which is ItemsModel.shared.reversedChatItems.reversed()
|
||||
return range
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
func loadLastItems(_ loadingMoreItems: Binding<Bool>, loadingBottomItems: Binding<Bool>, _ chat: Chat, _ im: ItemsModel) async {
|
||||
func loadLastItems(_ loadingMoreItems: Binding<Bool>, loadingBottomItems: Binding<Bool>, _ chat: Chat) async {
|
||||
await MainActor.run {
|
||||
loadingMoreItems.wrappedValue = true
|
||||
loadingBottomItems.wrappedValue = true
|
||||
|
@ -22,15 +22,27 @@ func loadLastItems(_ loadingMoreItems: Binding<Bool>, loadingBottomItems: Bindin
|
|||
}
|
||||
return
|
||||
}
|
||||
await apiLoadMessages(chat.chatInfo.id, im, ChatPagination.last(count: 50))
|
||||
await apiLoadMessages(chat.chatInfo.id, ChatPagination.last(count: 50), ItemsModel.shared.chatState)
|
||||
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,
|
||||
|
@ -38,7 +50,7 @@ func preloadIfNeeded(
|
|||
loadItems: @escaping (Bool, ChatPagination) async -> Bool,
|
||||
loadLastItems: @escaping () async -> Void
|
||||
) {
|
||||
let state = im.preloadState
|
||||
let state = PreloadState.shared
|
||||
guard !listState.isScrolling && !listState.isAnimatedScrolling,
|
||||
!state.preloading,
|
||||
listState.totalItemsCount > 0
|
||||
|
@ -51,7 +63,7 @@ func preloadIfNeeded(
|
|||
Task {
|
||||
defer { state.preloading = false }
|
||||
var triedToLoad = true
|
||||
await preloadItems(im, mergedItems.boxedValue, allowLoadMore, listState, ignoreLoadingRequests) { pagination in
|
||||
await preloadItems(mergedItems.boxedValue, allowLoadMore, listState, ignoreLoadingRequests) { pagination in
|
||||
triedToLoad = await loadItems(false, pagination)
|
||||
return triedToLoad
|
||||
}
|
||||
|
@ -61,11 +73,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 && !im.lastItemsLoaded {
|
||||
if listState.itemsCanCoverScreen && !ItemsModel.shared.lastItemsLoaded {
|
||||
await loadLastItems()
|
||||
}
|
||||
}
|
||||
} else if listState.itemsCanCoverScreen && !im.lastItemsLoaded {
|
||||
} else if listState.itemsCanCoverScreen && !ItemsModel.shared.lastItemsLoaded {
|
||||
state.preloading = true
|
||||
Task {
|
||||
defer { state.preloading = false }
|
||||
|
@ -75,7 +87,6 @@ func preloadIfNeeded(
|
|||
}
|
||||
|
||||
func preloadItems(
|
||||
_ im: ItemsModel,
|
||||
_ mergedItems: MergedItems,
|
||||
_ allowLoadMoreItems: Bool,
|
||||
_ listState: EndlessScrollView<MergedItem>.ListState,
|
||||
|
@ -94,7 +105,7 @@ async {
|
|||
let splits = mergedItems.splits
|
||||
let lastVisibleIndex = listState.lastVisibleItemIndex
|
||||
var lastIndexToLoadFrom: Int? = findLastIndexToLoadFromInSplits(firstVisibleIndex, lastVisibleIndex, remaining, splits)
|
||||
let items: [ChatItem] = im.reversedChatItems.reversed()
|
||||
let items: [ChatItem] = ItemsModel.shared.reversedChatItems.reversed()
|
||||
if splits.isEmpty && !items.isEmpty && lastVisibleIndex > mergedItems.items.count - remaining {
|
||||
lastIndexToLoadFrom = items.count - 1
|
||||
}
|
||||
|
@ -111,7 +122,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 == im.reversedChatItems.count && firstItemIdWas == im.reversedChatItems.last?.id {
|
||||
if triedToLoad && sizeWas == ItemsModel.shared.reversedChatItems.count && firstItemIdWas == ItemsModel.shared.reversedChatItems.last?.id {
|
||||
ignoreLoadingRequests.wrappedValue = loadFromItemId
|
||||
return false
|
||||
}
|
||||
|
@ -122,7 +133,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] = im.reversedChatItems
|
||||
let reversedItems: [ChatItem] = ItemsModel.shared.reversedChatItems
|
||||
if let split, split.indexRangeInParentItems.lowerBound + remaining > firstVisibleIndex {
|
||||
let index = split.indexRangeInReversed.lowerBound
|
||||
if index >= 0 {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -323,11 +323,11 @@ 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
|
||||
@Binding var selectedRange: NSRange
|
||||
var disabledText: LocalizedStringKey? = nil
|
||||
|
||||
@State var linkUrl: URL? = nil
|
||||
@State var hasSimplexLink: Bool = false
|
||||
|
@ -356,20 +356,6 @@ 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()
|
||||
|
@ -406,12 +392,12 @@ struct ComposeView: View {
|
|||
Image(systemName: "paperclip")
|
||||
.resizable()
|
||||
}
|
||||
.disabled(composeState.attachmentDisabled || !chat.userCanSend || (chat.chatInfo.contact?.nextSendGrpInv ?? false))
|
||||
.disabled(composeState.attachmentDisabled || !chat.chatInfo.sendMsgEnabled || (chat.chatInfo.contact?.nextSendGrpInv ?? false))
|
||||
.frame(width: 25, height: 25)
|
||||
.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(
|
||||
|
@ -456,32 +442,13 @@ struct ComposeView: View {
|
|||
: theme.colors.primary
|
||||
)
|
||||
.padding(.trailing, 12)
|
||||
.disabled(!chat.userCanSend)
|
||||
.disabled(!chat.chatInfo.sendMsgEnabled)
|
||||
|
||||
if im.secondaryIMFilter == nil {
|
||||
if chat.userIsPending {
|
||||
Text("reviewed by admins")
|
||||
.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."
|
||||
)
|
||||
}
|
||||
}
|
||||
if let disabledText {
|
||||
Text(disabledText)
|
||||
.italic()
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -507,8 +474,8 @@ struct ComposeView: View {
|
|||
hasSimplexLink = false
|
||||
}
|
||||
}
|
||||
.onChange(of: chat.userCanSend) { canSend in
|
||||
if !canSend {
|
||||
.onChange(of: chat.chatInfo.sendMsgEnabled) { sendEnabled in
|
||||
if !sendEnabled {
|
||||
cancelCurrentVoiceRecording()
|
||||
clearCurrentDraft()
|
||||
clearState()
|
||||
|
@ -972,7 +939,6 @@ 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
|
||||
|
@ -1035,9 +1001,9 @@ struct ComposeView: View {
|
|||
reportReason: reportReason,
|
||||
reportText: msgText
|
||||
) {
|
||||
if showReportsInSupportChatAlertDefault.get() {
|
||||
await MainActor.run {
|
||||
showReportsInSupportChatAlert()
|
||||
await MainActor.run {
|
||||
for chatItem in chatItems {
|
||||
chatModel.addChatItem(chat.chatInfo, chatItem)
|
||||
}
|
||||
}
|
||||
return chatItems.first
|
||||
|
@ -1045,27 +1011,7 @@ struct ComposeView: View {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func showReportsInSupportChatAlert() {
|
||||
showAlert(
|
||||
NSLocalizedString("Report sent to moderators", comment: "alert title"),
|
||||
message: NSLocalizedString("You can view your reports in Chat with admins.", comment: "alert message"),
|
||||
actions: {[
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Don't show again", comment: "alert action"),
|
||||
style: .default,
|
||||
handler: { _ in
|
||||
showReportsInSupportChatAlertDefault.set(false)
|
||||
}
|
||||
),
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Ok", comment: "alert action"),
|
||||
style: .default
|
||||
)
|
||||
]}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?, mentions: [String: Int64]) async -> ChatItem? {
|
||||
await send(
|
||||
[ComposedMessage(fileSource: file, quotedItemId: quoted, msgContent: mc, mentions: mentions)],
|
||||
|
@ -1080,7 +1026,6 @@ struct ComposeView: View {
|
|||
: await apiSendMessages(
|
||||
type: chat.chatInfo.chatType,
|
||||
id: chat.chatInfo.apiId,
|
||||
scope: chat.chatInfo.groupChatScope(),
|
||||
live: live,
|
||||
ttl: ttl,
|
||||
composedMessages: msgs
|
||||
|
@ -1105,10 +1050,8 @@ 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
|
||||
) {
|
||||
|
@ -1329,14 +1272,12 @@ 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),
|
||||
|
@ -1345,7 +1286,6 @@ struct ComposeView_Previews: PreviewProvider {
|
|||
.environmentObject(ChatModel())
|
||||
ComposeView(
|
||||
chat: chat,
|
||||
im: im,
|
||||
composeState: $composeState,
|
||||
keyboardVisible: Binding.constant(true),
|
||||
keyboardHiddenDate: Binding.constant(Date.now),
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
//
|
||||
// ContextPendingMemberActionsView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by spaced4ndy on 02.05.2025.
|
||||
// Copyright © 2025 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ContextPendingMemberActionsView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dismiss) var dismiss
|
||||
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, dismiss: dismiss)
|
||||
}
|
||||
|
||||
ZStack {
|
||||
Text("Accept")
|
||||
.foregroundColor(theme.colors.primary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
showAcceptMemberAlert(groupInfo, member, dismiss: dismiss)
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 54)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(.thinMaterial)
|
||||
}
|
||||
}
|
||||
|
||||
func showAcceptMemberAlert(_ groupInfo: GroupInfo, _ member: GroupMember, dismiss: DismissAction? = nil) {
|
||||
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, dismiss: dismiss)
|
||||
}
|
||||
),
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Accept as observer", comment: "alert action"),
|
||||
style: .default,
|
||||
handler: { _ in
|
||||
acceptMember(groupInfo, member, .observer, dismiss: dismiss)
|
||||
}
|
||||
),
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Cancel", comment: "alert action"),
|
||||
style: .default
|
||||
)
|
||||
]}
|
||||
)
|
||||
}
|
||||
|
||||
func acceptMember(_ groupInfo: GroupInfo, _ member: GroupMember, _ role: GroupMemberRole, dismiss: DismissAction? = nil) {
|
||||
Task {
|
||||
do {
|
||||
let (gInfo, acceptedMember) = try await apiAcceptMember(groupInfo.groupId, member.groupMemberId, role)
|
||||
await MainActor.run {
|
||||
_ = ChatModel.shared.upsertGroupMember(groupInfo, acceptedMember)
|
||||
ChatModel.shared.updateGroup(gInfo)
|
||||
dismiss?()
|
||||
}
|
||||
} 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
|
||||
)
|
||||
}
|
|
@ -15,6 +15,7 @@ struct SendMessageView: View {
|
|||
@Binding var composeState: ComposeState
|
||||
@Binding var selectedRange: NSRange
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.isEnabled) var isEnabled
|
||||
var sendMessage: (Int?) -> Void
|
||||
var sendLiveMessage: (() async -> Void)? = nil
|
||||
var updateLiveMessage: (() async -> Void)? = nil
|
||||
|
@ -255,6 +256,7 @@ struct SendMessageView: View {
|
|||
}
|
||||
|
||||
private struct RecordVoiceMessageButton: View {
|
||||
@Environment(\.isEnabled) var isEnabled
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var startVoiceMessageRecording: (() -> Void)?
|
||||
var finishVoiceMessageRecording: (() -> Void)?
|
||||
|
@ -263,11 +265,11 @@ struct SendMessageView: View {
|
|||
@State private var pressed: TimeInterval? = nil
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: "mic.fill")
|
||||
Image(systemName: isEnabled ? "mic.fill" : "mic")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(isEnabled ? theme.colors.primary : theme.colors.secondary)
|
||||
.opacity(holdingVMR ? 0.7 : 1)
|
||||
.disabled(disabled)
|
||||
.frame(width: 31, height: 31)
|
||||
|
@ -352,7 +354,7 @@ struct SendMessageView: View {
|
|||
Image(systemName: "bolt.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundColor(theme.colors.primary)
|
||||
.foregroundColor(isEnabled ? theme.colors.primary : theme.colors.secondary)
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
.frame(width: 29, height: 29)
|
||||
|
|
|
@ -78,12 +78,6 @@ struct AddGroupMembersViewCommon: View {
|
|||
let count = selectedContacts.count
|
||||
Section {
|
||||
if creatingGroup {
|
||||
MemberAdmissionButton(
|
||||
groupInfo: $groupInfo,
|
||||
admission: groupInfo.groupProfile.memberAdmission_,
|
||||
currentAdmission: groupInfo.groupProfile.memberAdmission_,
|
||||
creatingGroup: true
|
||||
)
|
||||
GroupPreferencesButton(
|
||||
groupInfo: $groupInfo,
|
||||
preferences: groupInfo.fullGroupPreferences,
|
||||
|
|
|
@ -17,7 +17,6 @@ struct GroupChatInfoView: View {
|
|||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var groupInfo: GroupInfo
|
||||
@Binding var scrollToItemId: ChatItem.ID?
|
||||
var onSearch: () -> Void
|
||||
@State var localAlias: String
|
||||
@FocusState private var aliasTextFieldFocused: Bool
|
||||
|
@ -88,25 +87,7 @@ struct GroupChatInfoView: View {
|
|||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
|
||||
Section {
|
||||
if groupInfo.canAddMembers && groupInfo.businessChat == nil {
|
||||
groupLinkButton()
|
||||
}
|
||||
if groupInfo.businessChat == nil && groupInfo.membership.memberRole >= .moderator {
|
||||
memberSupportButton()
|
||||
}
|
||||
if groupInfo.canModerate {
|
||||
GroupReportsChatNavLink(chat: chat, scrollToItemId: $scrollToItemId)
|
||||
}
|
||||
if groupInfo.membership.memberActive
|
||||
&& (groupInfo.membership.memberRole < .moderator || groupInfo.membership.supportChat != nil) {
|
||||
UserSupportChatNavLink(chat: chat, groupInfo: groupInfo, scrollToItemId: $scrollToItemId)
|
||||
}
|
||||
} header: {
|
||||
Text("")
|
||||
}
|
||||
|
||||
|
||||
Section {
|
||||
if groupInfo.isOwner && groupInfo.businessChat == nil {
|
||||
editGroupButton()
|
||||
|
@ -115,6 +96,19 @@ 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: {
|
||||
let label: LocalizedStringKey = (
|
||||
groupInfo.businessChat == nil
|
||||
|
@ -126,16 +120,6 @@ 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.")
|
||||
|
@ -143,6 +127,9 @@ struct GroupChatInfoView: View {
|
|||
|
||||
Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) {
|
||||
if groupInfo.canAddMembers {
|
||||
if groupInfo.businessChat == nil {
|
||||
groupLinkButton()
|
||||
}
|
||||
if (chat.chatInfo.incognito) {
|
||||
Label("Invite members", systemImage: "plus")
|
||||
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||
|
@ -157,16 +144,9 @@ struct GroupChatInfoView: View {
|
|||
let filteredMembers = s == ""
|
||||
? members
|
||||
: members.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) }
|
||||
MemberRowView(
|
||||
chat: chat,
|
||||
groupInfo: groupInfo,
|
||||
groupMember: GMember(groupInfo.membership),
|
||||
scrollToItemId: $scrollToItemId,
|
||||
user: true,
|
||||
alert: $alert
|
||||
)
|
||||
MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: GMember(groupInfo.membership), user: true, alert: $alert)
|
||||
ForEach(filteredMembers) { member in
|
||||
MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: member, scrollToItemId: $scrollToItemId, alert: $alert)
|
||||
MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: member, alert: $alert)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -175,7 +155,7 @@ struct GroupChatInfoView: View {
|
|||
if groupInfo.canDelete {
|
||||
deleteGroupButton()
|
||||
}
|
||||
if groupInfo.membership.memberCurrentOrPending {
|
||||
if groupInfo.membership.memberCurrent {
|
||||
leaveGroupButton()
|
||||
}
|
||||
}
|
||||
|
@ -373,7 +353,6 @@ struct GroupChatInfoView: View {
|
|||
var chat: Chat
|
||||
var groupInfo: GroupInfo
|
||||
@ObservedObject var groupMember: GMember
|
||||
@Binding var scrollToItemId: ChatItem.ID?
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var user: Bool = false
|
||||
@Binding var alert: GroupChatInfoViewAlert?
|
||||
|
@ -436,7 +415,7 @@ struct GroupChatInfoView: View {
|
|||
}
|
||||
|
||||
private func memberInfoView() -> some View {
|
||||
GroupMemberInfoView(groupInfo: groupInfo, chat: chat, groupMember: groupMember, scrollToItemId: $scrollToItemId)
|
||||
GroupMemberInfoView(groupInfo: groupInfo, chat: chat, groupMember: groupMember)
|
||||
.navigationBarHidden(false)
|
||||
}
|
||||
|
||||
|
@ -544,89 +523,6 @@ struct GroupChatInfoView: View {
|
|||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
|
||||
struct UserSupportChatNavLink: View {
|
||||
@ObservedObject var chat: Chat
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var groupInfo: GroupInfo
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Binding var scrollToItemId: ChatItem.ID?
|
||||
@State private var navLinkActive = false
|
||||
|
||||
var body: some View {
|
||||
let scopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: nil)
|
||||
NavigationLink(isActive: $navLinkActive) {
|
||||
SecondaryChatView(
|
||||
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo), chatItems: [], chatStats: ChatStats()),
|
||||
scrollToItemId: $scrollToItemId
|
||||
)
|
||||
} label: {
|
||||
HStack {
|
||||
Label("Chat with admins", systemImage: chat.supportUnreadCount > 0 ? "flag.fill" : "flag")
|
||||
Spacer()
|
||||
if chat.supportUnreadCount > 0 {
|
||||
UnreadBadge(count: chat.supportUnreadCount, color: theme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: navLinkActive) { active in
|
||||
if active {
|
||||
ItemsModel.loadSecondaryChat(groupInfo.id, chatFilter: .groupChatScopeContext(groupScopeInfo: scopeInfo))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func memberSupportButton() -> some View {
|
||||
NavigationLink {
|
||||
MemberSupportView(groupInfo: groupInfo, scrollToItemId: $scrollToItemId)
|
||||
.navigationBarTitle("Chats with members")
|
||||
.modifier(ThemedBackground())
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
HStack {
|
||||
Label(
|
||||
"Chats with members",
|
||||
systemImage: chat.supportUnreadCount > 0 ? "flag.fill" : "flag"
|
||||
)
|
||||
Spacer()
|
||||
if chat.supportUnreadCount > 0 {
|
||||
UnreadBadge(count: chat.supportUnreadCount, color: theme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupReportsChatNavLink: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@State private var navLinkActive = false
|
||||
@ObservedObject var chat: Chat
|
||||
@Binding var scrollToItemId: ChatItem.ID?
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(isActive: $navLinkActive) {
|
||||
SecondaryChatView(chat: chat, scrollToItemId: $scrollToItemId)
|
||||
} label: {
|
||||
HStack {
|
||||
Label {
|
||||
Text("Member reports")
|
||||
} icon: {
|
||||
Image(systemName: chat.chatStats.reportsCount > 0 ? "flag.fill" : "flag").foregroundColor(.red)
|
||||
}
|
||||
Spacer()
|
||||
if chat.chatStats.reportsCount > 0 {
|
||||
UnreadBadge(count: chat.chatStats.reportsCount, color: .red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: navLinkActive) { active in
|
||||
if active {
|
||||
ItemsModel.loadSecondaryChat(chat.id, chatFilter: .msgContentTagContext(contentTag: .report))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func editGroupButton() -> some View {
|
||||
NavigationLink {
|
||||
GroupProfileView(
|
||||
|
@ -787,36 +683,26 @@ struct GroupChatInfoView: View {
|
|||
title: Text("Remove member?"),
|
||||
message: Text(messageLabel),
|
||||
primaryButton: .destructive(Text("Remove")) {
|
||||
removeMember(groupInfo, mem)
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func removeMember(_ groupInfo: GroupInfo, _ mem: GroupMember, dismiss: DismissAction? = nil) {
|
||||
Task {
|
||||
do {
|
||||
let (updatedGroupInfo, updatedMembers) = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId])
|
||||
await MainActor.run {
|
||||
ChatModel.shared.updateGroup(updatedGroupInfo)
|
||||
updatedMembers.forEach { updatedMember in
|
||||
_ = ChatModel.shared.upsertGroupMember(updatedGroupInfo, updatedMember)
|
||||
}
|
||||
dismiss?()
|
||||
}
|
||||
} 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!")
|
||||
|
@ -910,7 +796,6 @@ struct GroupChatInfoView_Previews: PreviewProvider {
|
|||
GroupChatInfoView(
|
||||
chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []),
|
||||
groupInfo: Binding.constant(GroupInfo.sampleData),
|
||||
scrollToItemId: Binding.constant(nil),
|
||||
onSearch: {},
|
||||
localAlias: ""
|
||||
)
|
||||
|
|
|
@ -16,7 +16,6 @@ struct GroupMemberInfoView: View {
|
|||
@State var groupInfo: GroupInfo
|
||||
@ObservedObject var chat: Chat
|
||||
@ObservedObject var groupMember: GMember
|
||||
@Binding var scrollToItemId: ChatItem.ID?
|
||||
var navigation: Bool = false
|
||||
@State private var connectionStats: ConnectionStats? = nil
|
||||
@State private var connectionCode: String? = nil
|
||||
|
@ -104,10 +103,6 @@ struct GroupMemberInfoView: View {
|
|||
|
||||
if member.memberActive {
|
||||
Section {
|
||||
if groupInfo.membership.memberRole >= .moderator
|
||||
&& (member.memberRole < .moderator || member.supportChat != nil) {
|
||||
MemberInfoSupportChatNavLink(groupInfo: groupInfo, member: groupMember, scrollToItemId: $scrollToItemId)
|
||||
}
|
||||
if let code = connectionCode { verifyCodeButton(code) }
|
||||
if let connStats = connectionStats,
|
||||
connStats.ratchetSyncAllowed {
|
||||
|
@ -283,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
|
||||
}
|
||||
}
|
||||
|
@ -479,31 +474,6 @@ struct GroupMemberInfoView: View {
|
|||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
|
||||
struct MemberInfoSupportChatNavLink: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var groupInfo: GroupInfo
|
||||
var member: GMember
|
||||
@Binding var scrollToItemId: ChatItem.ID?
|
||||
@State private var navLinkActive = false
|
||||
|
||||
var body: some View {
|
||||
let scopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: member.wrapped)
|
||||
NavigationLink(isActive: $navLinkActive) {
|
||||
SecondaryChatView(
|
||||
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo), chatItems: [], chatStats: ChatStats()),
|
||||
scrollToItemId: $scrollToItemId
|
||||
)
|
||||
} label: {
|
||||
Label("Chat with member", systemImage: "flag")
|
||||
}
|
||||
.onChange(of: navLinkActive) { active in
|
||||
if active {
|
||||
ItemsModel.loadSecondaryChat(groupInfo.id, chatFilter: .groupChatScopeContext(groupScopeInfo: scopeInfo))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func verifyCodeButton(_ code: String) -> some View {
|
||||
let member = groupMember.wrapped
|
||||
return NavigationLink {
|
||||
|
@ -640,11 +610,10 @@ struct GroupMemberInfoView: View {
|
|||
primaryButton: .destructive(Text("Remove")) {
|
||||
Task {
|
||||
do {
|
||||
let (updatedGroupInfo, updatedMembers) = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId])
|
||||
let updatedMembers = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId])
|
||||
await MainActor.run {
|
||||
chatModel.updateGroup(updatedGroupInfo)
|
||||
updatedMembers.forEach { updatedMember in
|
||||
_ = chatModel.upsertGroupMember(updatedGroupInfo, updatedMember)
|
||||
_ = chatModel.upsertGroupMember(groupInfo, updatedMember)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
@ -852,8 +821,7 @@ struct GroupMemberInfoView_Previews: PreviewProvider {
|
|||
GroupMemberInfoView(
|
||||
groupInfo: GroupInfo.sampleData,
|
||||
chat: Chat.sampleData,
|
||||
groupMember: GMember.sampleData,
|
||||
scrollToItemId: Binding.constant(nil)
|
||||
groupMember: GMember.sampleData
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ let MAX_VISIBLE_MEMBER_ROWS: CGFloat = 4.8
|
|||
struct GroupMentionsView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var im: ItemsModel
|
||||
var groupInfo: GroupInfo
|
||||
@Binding var composeState: ComposeState
|
||||
@Binding var selectedRange: NSRange
|
||||
|
@ -94,31 +93,12 @@ struct GroupMentionsView: View {
|
|||
currentMessage = composeState.message
|
||||
}
|
||||
}
|
||||
|
||||
func contextMemberFilter(_ member: GroupMember) -> Bool {
|
||||
switch im.secondaryIMFilter {
|
||||
case nil:
|
||||
return true
|
||||
case let .groupChatScopeContext(groupScopeInfo):
|
||||
switch (groupScopeInfo) {
|
||||
case let .memberSupport(groupMember_):
|
||||
if let scopeMember = groupMember_ {
|
||||
return member.memberRole >= .moderator || member.groupMemberId == scopeMember.groupMemberId
|
||||
} else {
|
||||
return member.memberRole >= .moderator
|
||||
}
|
||||
}
|
||||
case .msgContentTagContext:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func filteredMembers() -> [GMember] {
|
||||
let s = mentionName.lowercased()
|
||||
return sortedMembers.filter {
|
||||
contextMemberFilter($0.wrapped)
|
||||
&& (s.isEmpty || $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s))
|
||||
}
|
||||
return s.isEmpty
|
||||
? sortedMembers
|
||||
: sortedMembers.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) }
|
||||
}
|
||||
|
||||
private func messageChanged(_ msg: String, _ parsedMsg: [FormattedText], _ range: NSRange) {
|
||||
|
|
|
@ -30,14 +30,6 @@ struct GroupPreferencesView: View {
|
|||
let saveText: LocalizedStringKey = creatingGroup ? "Save" : "Save and notify group members"
|
||||
VStack {
|
||||
List {
|
||||
Section {
|
||||
MemberAdmissionButton(
|
||||
groupInfo: $groupInfo,
|
||||
admission: groupInfo.groupProfile.memberAdmission_,
|
||||
currentAdmission: groupInfo.groupProfile.memberAdmission_,
|
||||
creatingGroup: creatingGroup
|
||||
)
|
||||
}
|
||||
featureSection(.timedMessages, $preferences.timedMessages.enable)
|
||||
featureSection(.fullDelete, $preferences.fullDelete.enable)
|
||||
featureSection(.directMessages, $preferences.directMessages.enable, $preferences.directMessages.role)
|
||||
|
@ -148,66 +140,6 @@ 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: {
|
||||
if creatingGroup {
|
||||
Text("Set member admission")
|
||||
} else {
|
||||
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))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupPreferencesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GroupPreferencesView(
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
//
|
||||
// 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 members", comment: "admission stage"),
|
||||
NSLocalizedString("Review members before admitting (\"knocking\").", 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)
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
} 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: {}
|
||||
)
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
//
|
||||
// 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.chatViewName).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())
|
||||
}
|
|
@ -1,265 +0,0 @@
|
|||
//
|
||||
// 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
|
||||
@Binding var scrollToItemId: ChatItem.ID?
|
||||
|
||||
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 chats with members")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
List {
|
||||
searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.onBackground, theme.colors.secondary)
|
||||
.padding(.leading, 8)
|
||||
ForEach(filteredMembersWithChats) { memberWithChat in
|
||||
MemberSupportChatNavLink(
|
||||
groupInfo: groupInfo,
|
||||
memberWithChat: memberWithChat,
|
||||
scrollToItemId: $scrollToItemId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MemberSupportChatNavLink: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@State private var memberSupportChatNavLinkActive = false
|
||||
var groupInfo: GroupInfo
|
||||
var memberWithChat: GMember
|
||||
@Binding var scrollToItemId: ChatItem.ID?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
let scopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: memberWithChat.wrapped)
|
||||
Button {
|
||||
ItemsModel.loadSecondaryChat(groupInfo.id, chatFilter: .groupChatScopeContext(groupScopeInfo: scopeInfo)) {
|
||||
memberSupportChatNavLinkActive = true
|
||||
}
|
||||
} label: {
|
||||
SupportChatRowView(groupMember: memberWithChat, groupInfo: groupInfo)
|
||||
}
|
||||
|
||||
NavigationLink(isActive: $memberSupportChatNavLinkActive) {
|
||||
SecondaryChatView(
|
||||
chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: scopeInfo), chatItems: [], chatStats: ChatStats()),
|
||||
scrollToItemId: $scrollToItemId
|
||||
)
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.frame(width: 1, height: 1)
|
||||
.hidden()
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
if memberWithChat.wrapped.memberPending {
|
||||
Button {
|
||||
showAcceptMemberAlert(groupInfo, memberWithChat.wrapped)
|
||||
} label: {
|
||||
Label("Accept", systemImage: "checkmark")
|
||||
}
|
||||
.tint(theme.colors.primary)
|
||||
}
|
||||
|
||||
Button {
|
||||
showRemoveMemberAlert(groupInfo, memberWithChat.wrapped)
|
||||
} label: {
|
||||
Label("Remove", systemImage: "trash")
|
||||
}
|
||||
.tint(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, dismiss: DismissAction? = nil) {
|
||||
showAlert(
|
||||
title: NSLocalizedString("Remove member?", comment: "alert title"),
|
||||
buttonTitle: "Remove",
|
||||
buttonAction: { removeMember(groupInfo, member, dismiss: dismiss) },
|
||||
cancelButton: true
|
||||
)
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MemberSupportView(
|
||||
groupInfo: GroupInfo.sampleData,
|
||||
scrollToItemId: Binding.constant(nil)
|
||||
)
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
//
|
||||
// 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
|
||||
@Binding var scrollToItemId: ChatItem.ID?
|
||||
|
||||
var body: some View {
|
||||
if let im = chatModel.secondaryIM {
|
||||
ChatView(
|
||||
chat: chat,
|
||||
im: im,
|
||||
mergedItems: BoxedValue(MergedItems.create(im, [])),
|
||||
floatingButtonModel: FloatingButtonModel(im: im),
|
||||
scrollToItemId: $scrollToItemId
|
||||
)
|
||||
.onDisappear {
|
||||
chatModel.secondaryIM = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SecondaryChatView(
|
||||
chat: Chat(
|
||||
chatInfo: .group(groupInfo: GroupInfo.sampleData, groupChatScope: .memberSupport(groupMember_: GroupMember.sampleData)),
|
||||
chatItems: [],
|
||||
chatStats: ChatStats()
|
||||
),
|
||||
scrollToItemId: Binding.constant(nil)
|
||||
)
|
||||
}
|
|
@ -25,7 +25,7 @@ struct SelectedItemsTopToolbar: View {
|
|||
struct SelectedItemsBottomToolbar: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let im: ItemsModel
|
||||
let chatItems: [ChatItem]
|
||||
@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 || im.secondaryIMFilter != nil ? theme.colors.secondary : .red)
|
||||
.foregroundColor(!moderateEnabled || deleteCountProhibited ? theme.colors.secondary : .red)
|
||||
}
|
||||
.disabled(!moderateEnabled || deleteCountProhibited || im.secondaryIMFilter != nil)
|
||||
.disabled(!moderateEnabled || deleteCountProhibited)
|
||||
.opacity(canModerate ? 1 : 0)
|
||||
|
||||
Spacer()
|
||||
|
@ -88,24 +88,24 @@ struct SelectedItemsBottomToolbar: View {
|
|||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20, alignment: .center)
|
||||
.foregroundColor(!forwardEnabled || forwardCountProhibited || im.secondaryIMFilter != nil ? theme.colors.secondary : theme.colors.primary)
|
||||
.foregroundColor(!forwardEnabled || forwardCountProhibited ? theme.colors.secondary : theme.colors.primary)
|
||||
}
|
||||
.disabled(!forwardEnabled || forwardCountProhibited || im.secondaryIMFilter != nil)
|
||||
.disabled(!forwardEnabled || forwardCountProhibited)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding([.leading, .trailing], 12)
|
||||
}
|
||||
.onAppear {
|
||||
recheckItems(chatInfo, im.reversedChatItems, selectedChatItems)
|
||||
recheckItems(chatInfo, chatItems, selectedChatItems)
|
||||
}
|
||||
.onChange(of: chatInfo) { info in
|
||||
recheckItems(info, im.reversedChatItems, selectedChatItems)
|
||||
recheckItems(info, chatItems, selectedChatItems)
|
||||
}
|
||||
.onChange(of: im.reversedChatItems) { items in
|
||||
.onChange(of: chatItems) { items in
|
||||
recheckItems(chatInfo, items, selectedChatItems)
|
||||
}
|
||||
.onChange(of: selectedChatItems) { selected in
|
||||
recheckItems(chatInfo, im.reversedChatItems, selected)
|
||||
recheckItems(chatInfo, chatItems, 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)
|
||||
|
@ -189,7 +189,7 @@ struct ChatListNavLink: View {
|
|||
}
|
||||
.swipeActions(edge: .trailing) {
|
||||
tagChatButton(chat)
|
||||
if (groupInfo.membership.memberCurrentOrPending) {
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
leaveGroupChatButton(groupInfo)
|
||||
}
|
||||
if groupInfo.canDelete {
|
||||
|
@ -214,7 +214,7 @@ struct ChatListNavLink: View {
|
|||
let showReportsButton = chat.chatStats.reportsCount > 0 && groupInfo.membership.memberRole >= .moderator
|
||||
let showClearButton = !chat.chatItems.isEmpty
|
||||
let showDeleteGroup = groupInfo.canDelete
|
||||
let showLeaveGroup = groupInfo.membership.memberCurrentOrPending
|
||||
let showLeaveGroup = groupInfo.membership.memberCurrent
|
||||
let totalNumberOfButtons = 1 + (showReportsButton ? 1 : 0) + (showClearButton ? 1 : 0) + (showDeleteGroup ? 1 : 0) + (showLeaveGroup ? 1 : 0)
|
||||
|
||||
if showClearButton && totalNumberOfButtons <= 3 {
|
||||
|
@ -276,7 +276,7 @@ struct ChatListNavLink: View {
|
|||
@ViewBuilder private func markReadButton() -> some View {
|
||||
if chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat {
|
||||
Button {
|
||||
Task { await markChatRead(ItemsModel.shared, chat) }
|
||||
Task { await markChatRead(chat) }
|
||||
} label: {
|
||||
SwipeLabel(NSLocalizedString("Read", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI)
|
||||
}
|
||||
|
@ -482,10 +482,12 @@ struct ChatListNavLink: View {
|
|||
.tint(theme.colors.primary)
|
||||
}
|
||||
.appSheet(isPresented: $showContactConnectionInfo) {
|
||||
if case let .contactConnection(contactConnection) = chat.chatInfo {
|
||||
ContactConnectionInfo(contactConnection: contactConnection)
|
||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
Group {
|
||||
if case let .contactConnection(contactConnection) = chat.chatInfo {
|
||||
ContactConnectionInfo(contactConnection: contactConnection)
|
||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
}
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
|
|
|
@ -148,7 +148,6 @@ struct ChatListView: View {
|
|||
@State private var userPickerShown: Bool = false
|
||||
@State private var sheet: SomeSheet<AnyView>? = nil
|
||||
@StateObject private var chatTagsModel = ChatTagsModel.shared
|
||||
@State private var scrollToItemId: ChatItem.ID? = nil
|
||||
|
||||
// iOS 15 is required it to show/hide toolbar while chat is hidden/visible
|
||||
@State private var viewOnScreen = true
|
||||
|
@ -447,14 +446,7 @@ struct ChatListView: View {
|
|||
|
||||
@ViewBuilder private func chatView() -> some View {
|
||||
if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) {
|
||||
let im = ItemsModel.shared
|
||||
ChatView(
|
||||
chat: chat,
|
||||
im: im,
|
||||
mergedItems: BoxedValue(MergedItems.create(im, [])),
|
||||
floatingButtonModel: FloatingButtonModel(im: im),
|
||||
scrollToItemId: $scrollToItemId
|
||||
)
|
||||
ChatView(chat: chat)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -900,12 +892,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)
|
||||
|
@ -341,12 +341,11 @@ 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)
|
||||
case .memAccepted: chatPreviewInfoText("connecting…")
|
||||
case .memPendingReview, .memPendingApproval: chatPreviewInfoText("reviewed by admins")
|
||||
default: EmptyView()
|
||||
}
|
||||
default: EmptyView()
|
||||
|
@ -440,11 +439,7 @@ struct ChatPreviewView: View {
|
|||
if progressByTimeout {
|
||||
ProgressView()
|
||||
} else if chat.chatStats.reportsCount > 0 {
|
||||
flagIcon(size: size * 0.8, color: .red)
|
||||
} else if chat.supportUnreadCount > 0 {
|
||||
flagIcon(size: size * 0.8, color: theme.colors.primary)
|
||||
} else if chat.chatInfo.groupInfo?.membership.memberPending ?? false {
|
||||
flagIcon(size: size * 0.8, color: theme.colors.secondary)
|
||||
groupReportsIcon(size: size * 0.8)
|
||||
} else {
|
||||
incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size)
|
||||
}
|
||||
|
@ -490,12 +485,12 @@ struct ChatPreviewView: View {
|
|||
}
|
||||
}
|
||||
|
||||
func flagIcon(size: CGFloat, color: Color) -> some View {
|
||||
func groupReportsIcon(size: CGFloat) -> some View {
|
||||
Image(systemName: "flag")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(color)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
|
||||
func smallContentPreview(size: CGFloat, _ view: @escaping () -> some View) -> some View {
|
||||
|
|
|
@ -124,7 +124,7 @@ struct UserPicker: View {
|
|||
ZStack(alignment: .topTrailing) {
|
||||
ProfileImage(imageStr: u.user.image, size: size, color: Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
if (u.unreadCount > 0) {
|
||||
userUnreadBadge(u, theme: theme).offset(x: 4, y: -4)
|
||||
UnreadBadge(userInfo: u).offset(x: 4, y: -4)
|
||||
}
|
||||
}
|
||||
.padding(.trailing, 6)
|
||||
|
@ -171,27 +171,19 @@ struct UserPicker: View {
|
|||
}
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
func userUnreadBadge(_ userInfo: UserInfo, theme: AppTheme) -> some View {
|
||||
UnreadBadge(
|
||||
count: userInfo.unreadCount,
|
||||
color: userInfo.user.showNtfs ? theme.colors.primary : theme.colors.secondary
|
||||
)
|
||||
}
|
||||
|
||||
struct UnreadBadge: View {
|
||||
var userInfo: UserInfo
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
|
||||
var count: Int
|
||||
var color: Color
|
||||
|
||||
var body: some View {
|
||||
let size = dynamicSize(userFont).chatInfoSize
|
||||
unreadCountText(count)
|
||||
unreadCountText(userInfo.unreadCount)
|
||||
.font(userFont <= .xxxLarge ? .caption : .caption2)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, dynamicSize(userFont).unreadPadding)
|
||||
.frame(minWidth: size, minHeight: size)
|
||||
.background(color)
|
||||
.background(userInfo.user.showNtfs ? theme.colors.primary : theme.colors.secondary)
|
||||
.cornerRadius(dynamicSize(userFont).unreadCorner)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ extension View {
|
|||
func appSheet<Content>(
|
||||
isPresented: Binding<Bool>,
|
||||
onDismiss: (() -> Void)? = nil,
|
||||
@ViewBuilder content: @escaping () -> Content
|
||||
content: @escaping () -> Content
|
||||
) -> some View where Content: View {
|
||||
sheet(isPresented: isPresented, onDismiss: onDismiss) {
|
||||
content().modifier(PrivacySensitive())
|
||||
|
@ -43,7 +43,7 @@ extension View {
|
|||
func appSheet<T, Content>(
|
||||
item: Binding<T?>,
|
||||
onDismiss: (() -> Void)? = nil,
|
||||
@ViewBuilder content: @escaping (T) -> Content
|
||||
content: @escaping (T) -> Content
|
||||
) -> some View where T: Identifiable, Content: View {
|
||||
sheet(item: item, onDismiss: onDismiss) { it in
|
||||
content(it).modifier(PrivacySensitive())
|
||||
|
|
|
@ -66,8 +66,6 @@ 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, groupChatScope: nil), chatItems: [])
|
||||
let c = Chat(chatInfo: .group(groupInfo: gInfo), 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, im: ItemsModel.shared, chatItem: alice, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: alice, scrollToItemId: { _ in })
|
||||
.modifier(ChatItemClipped(alice, tailVisible: true))
|
||||
Spacer()
|
||||
}
|
||||
HStack {
|
||||
Spacer()
|
||||
ChatItemView(chat: Chat.sampleData, im: ItemsModel.shared, chatItem: bob, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
|
||||
ChatItemView(chat: Chat.sampleData, chatItem: bob, scrollToItemId: { _ in })
|
||||
.modifier(ChatItemClipped(bob, tailVisible: true))
|
||||
.frame(alignment: .trailing)
|
||||
}
|
||||
|
|
|
@ -58,7 +58,6 @@ 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,7 +117,6 @@ 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,
|
||||
|
@ -146,7 +144,6 @@ 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
|
||||
]
|
||||
|
@ -198,8 +195,6 @@ let customDisappearingMessageTimeDefault = IntDefault(defaults: UserDefaults.sta
|
|||
let showDeleteConversationNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE)
|
||||
let showDeleteContactNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONTACT_NOTICE)
|
||||
|
||||
let showReportsInSupportChatAlertDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT)
|
||||
|
||||
/// after importing new database, this flag will be set and unset only after importing app settings in `initializeChat` */
|
||||
let shouldImportAppSettingsDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOULD_IMPORT_APP_SETTINGS)
|
||||
let currentThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME, withDefault: DefaultTheme.SYSTEM_THEME_NAME)
|
||||
|
|
|
@ -350,7 +350,7 @@ struct UserProfilesView: View {
|
|||
Image(systemName: "checkmark").foregroundColor(theme.colors.onBackground)
|
||||
} else {
|
||||
if userInfo.unreadCount > 0 {
|
||||
userUnreadBadge(userInfo, theme: theme)
|
||||
UnreadBadge(userInfo: userInfo)
|
||||
}
|
||||
if user.hidden {
|
||||
Image(systemName: "lock").foregroundColor(theme.colors.secondary)
|
||||
|
|
|
@ -67,7 +67,6 @@ func apiSendMessages(
|
|||
: SEChatCommand.apiSendMessages(
|
||||
type: chatInfo.chatType,
|
||||
id: chatInfo.apiId,
|
||||
scope: chatInfo.groupChatScope(),
|
||||
live: false,
|
||||
ttl: nil,
|
||||
composedMessages: composedMessages
|
||||
|
@ -124,7 +123,7 @@ enum SEChatCommand: ChatCmdProtocol {
|
|||
case apiSetEncryptLocalFiles(enable: Bool)
|
||||
case apiGetChats(userId: Int64)
|
||||
case apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage])
|
||||
case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
|
||||
case apiSendMessages(type: ChatType, id: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
|
||||
|
||||
var cmdString: String {
|
||||
switch self {
|
||||
|
@ -140,27 +139,15 @@ enum SEChatCommand: ChatCmdProtocol {
|
|||
case let .apiCreateChatItems(noteFolderId, composedMessages):
|
||||
let msgs = encodeJSON(composedMessages)
|
||||
return "/_create *\(noteFolderId) json \(msgs)"
|
||||
case let .apiSendMessages(type, id, scope, live, ttl, composedMessages):
|
||||
case let .apiSendMessages(type, id, live, ttl, composedMessages):
|
||||
let msgs = encodeJSON(composedMessages)
|
||||
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
|
||||
return "/_send \(ref(type, id, scope: scope)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
|
||||
return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
|
||||
}
|
||||
}
|
||||
|
||||
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 ref(_ type: ChatType, _ id: Int64) -> String {
|
||||
"\(type.rawValue)\(id)"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -168,19 +168,14 @@
|
|||
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 */; };
|
||||
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
|
||||
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; };
|
||||
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; };
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.1-7Y3Lr8U6bNmEaeIx88dGf7-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.1-7Y3Lr8U6bNmEaeIx88dGf7-ghc9.6.3.a */; };
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.1-7Y3Lr8U6bNmEaeIx88dGf7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.1-7Y3Lr8U6bNmEaeIx88dGf7.a */; };
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a */; };
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a */; };
|
||||
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; };
|
||||
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
|
||||
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
|
||||
|
@ -532,19 +527,14 @@
|
|||
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>"; };
|
||||
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
|
||||
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.1-7Y3Lr8U6bNmEaeIx88dGf7-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.1-7Y3Lr8U6bNmEaeIx88dGf7-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.1-7Y3Lr8U6bNmEaeIx88dGf7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.1-7Y3Lr8U6bNmEaeIx88dGf7.a"; sourceTree = "<group>"; };
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a"; sourceTree = "<group>"; };
|
||||
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
|
||||
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
|
||||
|
@ -702,8 +692,8 @@
|
|||
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
|
||||
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
|
||||
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.1-7Y3Lr8U6bNmEaeIx88dGf7-ghc9.6.3.a in Frameworks */,
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.1-7Y3Lr8U6bNmEaeIx88dGf7.a in Frameworks */,
|
||||
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a in Frameworks */,
|
||||
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a in Frameworks */,
|
||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -788,8 +778,8 @@
|
|||
64C829992D54AEEE006B9E89 /* libffi.a */,
|
||||
64C829982D54AEED006B9E89 /* libgmp.a */,
|
||||
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.1-7Y3Lr8U6bNmEaeIx88dGf7-ghc9.6.3.a */,
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.1-7Y3Lr8U6bNmEaeIx88dGf7.a */,
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a */,
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1086,7 +1076,6 @@
|
|||
644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */,
|
||||
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */,
|
||||
6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */,
|
||||
64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */,
|
||||
);
|
||||
path = ComposeMessage;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1128,10 +1117,6 @@
|
|||
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>";
|
||||
|
@ -1446,10 +1431,8 @@
|
|||
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 */,
|
||||
|
@ -1484,7 +1467,6 @@
|
|||
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 */,
|
||||
|
@ -1527,7 +1509,6 @@
|
|||
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 */,
|
||||
|
@ -1606,7 +1587,6 @@
|
|||
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 */,
|
||||
|
@ -1991,7 +1971,7 @@
|
|||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 278;
|
||||
CURRENT_PROJECT_VERSION = 282;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
|
@ -2016,7 +1996,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES_THIN;
|
||||
MARKETING_VERSION = 6.4;
|
||||
MARKETING_VERSION = 6.3.6;
|
||||
OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
|
@ -2041,7 +2021,7 @@
|
|||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 278;
|
||||
CURRENT_PROJECT_VERSION = 282;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
|
@ -2066,7 +2046,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.4;
|
||||
MARKETING_VERSION = 6.3.6;
|
||||
OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
|
@ -2083,11 +2063,11 @@
|
|||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 278;
|
||||
CURRENT_PROJECT_VERSION = 282;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.4;
|
||||
MARKETING_VERSION = 6.3.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -2103,11 +2083,11 @@
|
|||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 278;
|
||||
CURRENT_PROJECT_VERSION = 282;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.4;
|
||||
MARKETING_VERSION = 6.3.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -2128,7 +2108,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 278;
|
||||
CURRENT_PROJECT_VERSION = 282;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
|
@ -2143,7 +2123,7 @@
|
|||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.4;
|
||||
MARKETING_VERSION = 6.3.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -2165,7 +2145,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 278;
|
||||
CURRENT_PROJECT_VERSION = 282;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
|
@ -2180,7 +2160,7 @@
|
|||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.4;
|
||||
MARKETING_VERSION = 6.3.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
@ -2202,7 +2182,7 @@
|
|||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 278;
|
||||
CURRENT_PROJECT_VERSION = 282;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
|
@ -2228,7 +2208,7 @@
|
|||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.4;
|
||||
MARKETING_VERSION = 6.3.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -2253,7 +2233,7 @@
|
|||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 278;
|
||||
CURRENT_PROJECT_VERSION = 282;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
|
@ -2279,7 +2259,7 @@
|
|||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.4;
|
||||
MARKETING_VERSION = 6.3.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -2304,7 +2284,7 @@
|
|||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 278;
|
||||
CURRENT_PROJECT_VERSION = 282;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
|
@ -2319,7 +2299,7 @@
|
|||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.4;
|
||||
MARKETING_VERSION = 6.3.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -2338,7 +2318,7 @@
|
|||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 278;
|
||||
CURRENT_PROJECT_VERSION = 282;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
|
@ -2353,7 +2333,7 @@
|
|||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.4;
|
||||
MARKETING_VERSION = 6.3.6;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
|
|
@ -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, groupChatScope: GroupChatScopeInfo?)
|
||||
case group(groupInfo: GroupInfo)
|
||||
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
|
||||
|
@ -1333,11 +1333,24 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
|||
}
|
||||
}
|
||||
|
||||
public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? {
|
||||
get {
|
||||
switch self {
|
||||
case let .direct(contact): return contact.userCantSendReason
|
||||
case let .group(groupInfo): return groupInfo.userCantSendReason
|
||||
case let .local(noteFolder): return noteFolder.userCantSendReason
|
||||
case let .contactRequest(contactRequest): return contactRequest.userCantSendReason
|
||||
case let .contactConnection(contactConnection): return contactConnection.userCantSendReason
|
||||
case .invalidJSON: return ("can't send messages", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var sendMsgEnabled: Bool {
|
||||
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 +1363,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 +1388,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 +1405,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 +1428,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 +1453,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,14 +1484,7 @@ 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)
|
||||
}
|
||||
|
@ -1494,7 +1500,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
|
||||
}
|
||||
}
|
||||
|
@ -1510,7 +1516,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
|
||||
}
|
||||
}
|
||||
|
@ -1518,7 +1524,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
|
||||
|
@ -1529,7 +1535,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
|
||||
|
@ -1540,7 +1546,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
|
||||
|
@ -1556,7 +1562,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 {
|
||||
|
@ -1576,7 +1582,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
|||
|
||||
public static var sampleData: ChatInfo.SampleData = SampleData(
|
||||
direct: ChatInfo.direct(contact: Contact.sampleData),
|
||||
group: ChatInfo.group(groupInfo: GroupInfo.sampleData, groupChatScope: nil),
|
||||
group: ChatInfo.group(groupInfo: GroupInfo.sampleData),
|
||||
local: ChatInfo.local(noteFolder: NoteFolder.sampleData),
|
||||
contactRequest: ChatInfo.contactRequest(contactRequest: UserContactRequest.sampleData),
|
||||
contactConnection: ChatInfo.contactConnection(contactConnection: PendingContactConnection.getSampleData())
|
||||
|
@ -1606,13 +1612,7 @@ 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, minUnreadItemId: Int64 = 0, unreadChat: Bool = false) {
|
||||
self.unreadCount = unreadCount
|
||||
self.unreadMentions = unreadMentions
|
||||
self.reportsCount = reportsCount
|
||||
|
@ -1629,32 +1629,6 @@ public struct ChatStats: Decodable, Hashable {
|
|||
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
|
||||
|
@ -1681,15 +1655,16 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable {
|
|||
public var ready: Bool { get { activeConn?.connStatus == .ready } }
|
||||
public var sndReady: Bool { get { ready || activeConn?.connStatus == .sndReady } }
|
||||
public var active: Bool { get { contactStatus == .active } }
|
||||
public var sendMsgEnabled: Bool { get {
|
||||
(
|
||||
sndReady
|
||||
&& active
|
||||
&& !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false)
|
||||
&& !(activeConn?.connDisabled ?? true)
|
||||
)
|
||||
|| nextSendGrpInv
|
||||
} }
|
||||
public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? {
|
||||
// TODO [short links] this will have additional statuses for pending contact requests before they are accepted
|
||||
if nextSendGrpInv { return nil }
|
||||
if !active { return ("contact deleted", nil) }
|
||||
if !sndReady { return ("contact not ready", nil) }
|
||||
if activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false { return ("not synchronized", nil) }
|
||||
if activeConn?.connDisabled ?? true { return ("contact disabled", nil) }
|
||||
return nil
|
||||
}
|
||||
public var sendMsgEnabled: Bool { userCantSendReason == nil }
|
||||
public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } }
|
||||
public var displayName: String { localAlias == "" ? profile.displayName : localAlias }
|
||||
public var fullName: String { get { profile.fullName } }
|
||||
|
@ -1868,6 +1843,7 @@ public struct UserContactRequest: Decodable, NamedChat, Hashable {
|
|||
public var id: ChatId { get { "<@\(contactRequestId)" } }
|
||||
public var apiId: Int64 { get { contactRequestId } }
|
||||
var ready: Bool { get { true } }
|
||||
public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { ("can't send messages", nil) }
|
||||
public var sendMsgEnabled: Bool { get { false } }
|
||||
public var displayName: String { get { profile.displayName } }
|
||||
public var fullName: String { get { profile.fullName } }
|
||||
|
@ -1900,6 +1876,7 @@ public struct PendingContactConnection: Decodable, NamedChat, Hashable {
|
|||
public var id: ChatId { get { ":\(pccConnId)" } }
|
||||
public var apiId: Int64 { get { pccConnId } }
|
||||
var ready: Bool { get { false } }
|
||||
public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { ("can't send messages", nil) }
|
||||
public var sendMsgEnabled: Bool { get { false } }
|
||||
var localDisplayName: String {
|
||||
get { String.localizedStringWithFormat(NSLocalizedString("connection:%@", comment: "connection information"), pccConnId) }
|
||||
|
@ -2025,12 +2002,24 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
|
|||
var updatedAt: Date
|
||||
var chatTs: Date?
|
||||
public var uiThemes: ThemeModeOverrides?
|
||||
public var membersRequireAttention: Int
|
||||
|
||||
public var id: ChatId { get { "#\(groupId)" } }
|
||||
public var apiId: Int64 { get { groupId } }
|
||||
public var ready: Bool { get { true } }
|
||||
public var sendMsgEnabled: Bool { get { membership.memberActive } }
|
||||
public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? {
|
||||
return if membership.memberActive {
|
||||
membership.memberRole == .observer ? ("you are observer", "Please contact group admin.") : nil
|
||||
} else {
|
||||
switch membership.memberStatus {
|
||||
case .memRejected: ("request to join rejected", nil)
|
||||
case .memGroupDeleted: ("group is deleted", nil)
|
||||
case .memRemoved: ("removed from group", nil)
|
||||
case .memLeft: ("you left", nil)
|
||||
default: ("can't send messages", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
public var sendMsgEnabled: Bool { userCantSendReason == nil }
|
||||
public var displayName: String { localAlias == "" ? groupProfile.displayName : localAlias }
|
||||
public var fullName: String { get { groupProfile.fullName } }
|
||||
public var image: String? { get { groupProfile.image } }
|
||||
|
@ -2043,17 +2032,13 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
|
|||
}
|
||||
|
||||
public var canDelete: Bool {
|
||||
return membership.memberRole == .owner || !membership.memberCurrentOrPending
|
||||
return membership.memberRole == .owner || !membership.memberCurrent
|
||||
}
|
||||
|
||||
public var canAddMembers: Bool {
|
||||
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",
|
||||
|
@ -2063,7 +2048,6 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
|
|||
chatSettings: ChatSettings.defaults,
|
||||
createdAt: .now,
|
||||
updatedAt: .now,
|
||||
membersRequireAttention: 0,
|
||||
chatTags: [],
|
||||
localAlias: ""
|
||||
)
|
||||
|
@ -2075,20 +2059,12 @@ 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,
|
||||
memberAdmission: GroupMemberAdmission? = nil
|
||||
) {
|
||||
public init(displayName: String, fullName: String, description: String? = nil, image: String? = nil, groupPreferences: GroupPreferences? = nil) {
|
||||
self.displayName = displayName
|
||||
self.fullName = fullName
|
||||
self.description = description
|
||||
self.image = image
|
||||
self.groupPreferences = groupPreferences
|
||||
self.memberAdmission = memberAdmission
|
||||
}
|
||||
|
||||
public var displayName: String
|
||||
|
@ -2096,48 +2072,14 @@ 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 var memberAdmission_: GroupMemberAdmission {
|
||||
get { self.memberAdmission ?? GroupMemberAdmission() }
|
||||
set { memberAdmission = newValue }
|
||||
}
|
||||
|
||||
public static let sampleData = GroupProfile(
|
||||
displayName: "team",
|
||||
fullName: "My Team"
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -2164,7 +2106,6 @@ 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)" }
|
||||
|
@ -2236,7 +2177,6 @@ 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
|
||||
|
@ -2256,7 +2196,6 @@ 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
|
||||
|
@ -2267,18 +2206,6 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
|
|||
}
|
||||
}
|
||||
|
||||
public var memberPending: Bool {
|
||||
switch memberStatus {
|
||||
case .memPendingApproval: return true
|
||||
case .memPendingReview: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
public var memberCurrentOrPending: Bool {
|
||||
memberCurrent || memberPending
|
||||
}
|
||||
|
||||
public func canBeRemoved(groupInfo: GroupInfo) -> Bool {
|
||||
let userRole = groupInfo.membership.memberRole
|
||||
return memberStatus != .memRemoved && memberStatus != .memLeft
|
||||
|
@ -2332,13 +2259,6 @@ 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
|
||||
}
|
||||
|
@ -2408,7 +2328,6 @@ 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"
|
||||
|
@ -2426,7 +2345,6 @@ 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)"
|
||||
|
@ -2446,7 +2364,6 @@ 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"
|
||||
|
@ -2469,6 +2386,7 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat, Hashable {
|
|||
public var id: ChatId { get { "*\(noteFolderId)" } }
|
||||
public var apiId: Int64 { get { noteFolderId } }
|
||||
public var ready: Bool { get { true } }
|
||||
public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { nil }
|
||||
public var sendMsgEnabled: Bool { get { true } }
|
||||
public var displayName: String { get { ChatInfo.privateNotesChatName } }
|
||||
public var fullName: String { get { "" } }
|
||||
|
@ -2729,15 +2647,12 @@ 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 .memberAccepted: nil
|
||||
case .userPendingReview: nil
|
||||
default: .sndGroupEvent
|
||||
}
|
||||
default:
|
||||
|
@ -2770,8 +2685,6 @@ 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
|
||||
|
@ -2783,7 +2696,6 @@ 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
|
||||
|
@ -2852,12 +2764,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
|
||||
|
@ -3239,21 +3151,6 @@ 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
|
||||
|
@ -3307,17 +3204,6 @@ 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
|
||||
|
@ -4217,16 +4103,6 @@ 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?
|
||||
|
@ -4564,8 +4440,6 @@ 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)
|
||||
|
@ -4577,16 +4451,12 @@ 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)
|
||||
|
@ -4606,7 +4476,6 @@ 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")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4631,8 +4500,6 @@ public enum SndGroupEvent: Decodable, Hashable {
|
|||
case memberDeleted(groupMemberId: Int64, profile: Profile)
|
||||
case userLeft
|
||||
case groupUpdated(groupProfile: GroupProfile)
|
||||
case memberAccepted(groupMemberId: Int64, profile: Profile)
|
||||
case userPendingReview
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
|
@ -4650,9 +4517,6 @@ 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 .memberAccepted: return NSLocalizedString("you accepted this member", 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):"
|
||||
|
|
|
@ -155,6 +155,7 @@ buildConfig {
|
|||
buildConfigField("String", "DESKTOP_VERSION_NAME", "\"${extra["desktop.version_name"]}\"")
|
||||
buildConfigField("int", "DESKTOP_VERSION_CODE", "${extra["desktop.version_code"]}")
|
||||
buildConfigField("String", "DATABASE_BACKEND", "\"${extra["database.backend"]}\"")
|
||||
buildConfigField("Boolean", "ANDROID_BUNDLE", "${extra["android.bundle"]}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,6 @@ import chat.simplex.common.views.helpers.*
|
|||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import java.lang.reflect.Field
|
||||
import java.net.URI
|
||||
|
@ -51,11 +50,10 @@ import java.net.URI
|
|||
actual fun PlatformTextField(
|
||||
composeState: MutableState<ComposeState>,
|
||||
sendMsgEnabled: Boolean,
|
||||
disabledText: String?,
|
||||
sendMsgButtonDisabled: Boolean,
|
||||
textStyle: MutableState<TextStyle>,
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
userIsPending: Boolean,
|
||||
placeholder: String,
|
||||
showVoiceButton: Boolean,
|
||||
onMessageChange: (ComposeMessage) -> Unit,
|
||||
|
@ -198,18 +196,16 @@ actual fun PlatformTextField(
|
|||
showDeleteTextButton.value = it.lineCount >= 4 && !cs.inProgress
|
||||
}
|
||||
if (composeState.value.preview is ComposePreview.VoicePreview) {
|
||||
ComposeOverlay(MR.strings.voice_message_send_text, textStyle, padding)
|
||||
} else if (userIsPending) {
|
||||
ComposeOverlay(MR.strings.reviewed_by_admins, textStyle, padding)
|
||||
} else if (userIsObserver) {
|
||||
ComposeOverlay(MR.strings.you_are_observer, textStyle, padding)
|
||||
ComposeOverlay(generalGetString(MR.strings.voice_message_send_text), textStyle, padding)
|
||||
} else if (disabledText != null) {
|
||||
ComposeOverlay(disabledText, textStyle, padding)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ComposeOverlay(textId: StringResource, textStyle: MutableState<TextStyle>, padding: PaddingValues) {
|
||||
private fun ComposeOverlay(text: String, textStyle: MutableState<TextStyle>, padding: PaddingValues) {
|
||||
Text(
|
||||
generalGetString(textId),
|
||||
text,
|
||||
Modifier.padding(padding),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
style = textStyle.value.copy(fontStyle = FontStyle.Italic)
|
||||
|
|
|
@ -91,7 +91,7 @@ fun UserPickerUserBox(
|
|||
ProfileImageForActiveCall(size = USER_PICKER_IMAGE_SIZE, image = userInfo.user.profile.image, color = MaterialTheme.colors.secondaryVariant)
|
||||
|
||||
if (userInfo.unreadCount > 0 && !userInfo.user.activeUser) {
|
||||
userUnreadBadge(userInfo.unreadCount, userInfo.user.showNtfs, false)
|
||||
unreadBadge(userInfo.unreadCount, userInfo.user.showNtfs, false)
|
||||
}
|
||||
}
|
||||
val user = userInfo.user
|
||||
|
|
|
@ -295,13 +295,14 @@ object ChatModel {
|
|||
}
|
||||
}
|
||||
|
||||
class ChatsContext(val secondaryContextFilter: SecondaryContextFilter?) {
|
||||
class ChatsContext(val contentTag: MsgContentTag?) {
|
||||
val chats = mutableStateOf(SnapshotStateList<Chat>())
|
||||
/** if you modify the items by adding/removing them, use helpers methods like [addToChatItems], [removeLastChatItems], [removeAllAndNotify], [clearAndNotify] and so on.
|
||||
* If some helper is missing, create it. Notify is needed to track state of items that we added manually (not via api call). See [apiLoadMessages].
|
||||
* If you use api call to get the items, use just [add] instead of [addToChatItems].
|
||||
* Never modify underlying list directly because it produces unexpected results in ChatView's LazyColumn (setting by index is ok) */
|
||||
val chatItems = mutableStateOf(SnapshotStateList<ChatItem>())
|
||||
val chatItemStatuses = mutableMapOf<Long, CIStatus>()
|
||||
// set listener here that will be notified on every add/delete of a chat item
|
||||
val chatState = ActiveChatState()
|
||||
|
||||
|
@ -309,26 +310,6 @@ object ChatModel {
|
|||
fun getChat(id: String): Chat? = chats.value.firstOrNull { it.id == id }
|
||||
private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId }
|
||||
|
||||
val contentTag: MsgContentTag? =
|
||||
when (secondaryContextFilter) {
|
||||
null -> null
|
||||
is SecondaryContextFilter.GroupChatScopeContext -> null
|
||||
is SecondaryContextFilter.MsgContentTagContext -> secondaryContextFilter.contentTag
|
||||
}
|
||||
|
||||
val groupScopeInfo: GroupChatScopeInfo? =
|
||||
when (secondaryContextFilter) {
|
||||
null -> null
|
||||
is SecondaryContextFilter.GroupChatScopeContext -> secondaryContextFilter.groupScopeInfo
|
||||
is SecondaryContextFilter.MsgContentTagContext -> null
|
||||
}
|
||||
|
||||
val isUserSupportChat: Boolean =
|
||||
when (groupScopeInfo) {
|
||||
null -> false
|
||||
is GroupChatScopeInfo.MemberSupport -> groupScopeInfo.groupMember_ == null
|
||||
}
|
||||
|
||||
suspend fun addChat(chat: Chat) {
|
||||
chats.add(index = 0, chat)
|
||||
popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = 0)
|
||||
|
@ -361,8 +342,6 @@ object ChatModel {
|
|||
)
|
||||
)
|
||||
}
|
||||
} else if (currentCInfo is ChatInfo.Group && newCInfo is ChatInfo.Group && newCInfo.groupChatScope != null) {
|
||||
newCInfo = newCInfo.copy(groupInfo = newCInfo.groupInfo, groupChatScope = null)
|
||||
}
|
||||
chats[i] = chats[i].copy(chatInfo = newCInfo)
|
||||
}
|
||||
|
@ -385,7 +364,7 @@ object ChatModel {
|
|||
updateContact(rhId, updatedContact)
|
||||
}
|
||||
|
||||
suspend fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo, groupChatScope = null))
|
||||
suspend fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo))
|
||||
|
||||
private suspend fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) {
|
||||
if (hasChat(rhId, cInfo.id)) {
|
||||
|
@ -437,64 +416,55 @@ object ChatModel {
|
|||
}
|
||||
|
||||
suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
|
||||
// updates membersRequireAttention
|
||||
updateChatInfo(rhId, cInfo)
|
||||
// mark chat non deleted
|
||||
if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) {
|
||||
val updatedContact = cInfo.contact.copy(chatDeleted = false)
|
||||
updateContact(rhId, updatedContact)
|
||||
}
|
||||
// update chat list
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
if (i >= 0) {
|
||||
chat = chatsContext.chats[i]
|
||||
// update preview (for chat from main scope to show new items for invitee in pending status)
|
||||
if (cInfo.groupChatScope() == null || cInfo.groupInfo_?.membership?.memberPending == true) {
|
||||
val newPreviewItem = when (cInfo) {
|
||||
is ChatInfo.Group -> {
|
||||
val currentPreviewItem = chat.chatItems.firstOrNull()
|
||||
if (currentPreviewItem != null) {
|
||||
if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) {
|
||||
cItem
|
||||
} else {
|
||||
currentPreviewItem
|
||||
}
|
||||
} else {
|
||||
chat = chats[i]
|
||||
val newPreviewItem = when (cInfo) {
|
||||
is ChatInfo.Group -> {
|
||||
val currentPreviewItem = chat.chatItems.firstOrNull()
|
||||
if (currentPreviewItem != null) {
|
||||
if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) {
|
||||
cItem
|
||||
} else {
|
||||
currentPreviewItem
|
||||
}
|
||||
} else {
|
||||
cItem
|
||||
}
|
||||
|
||||
else -> cItem
|
||||
}
|
||||
val wasUnread = chat.unreadTag
|
||||
chatsContext.chats[i] = chat.copy(
|
||||
chatItems = arrayListOf(newPreviewItem),
|
||||
chatStats =
|
||||
if (cItem.meta.itemStatus is CIStatus.RcvNew) {
|
||||
increaseUnreadCounter(rhId, currentUser.value!!)
|
||||
chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, unreadMentions = if (cItem.meta.userMention) chat.chatStats.unreadMentions + 1 else chat.chatStats.unreadMentions)
|
||||
} else
|
||||
chat.chatStats
|
||||
)
|
||||
updateChatTagReadInPrimaryContext(chatsContext.chats[i], wasUnread)
|
||||
else -> cItem
|
||||
}
|
||||
// pop chat
|
||||
val wasUnread = chat.unreadTag
|
||||
chats[i] = chat.copy(
|
||||
chatItems = arrayListOf(newPreviewItem),
|
||||
chatStats =
|
||||
if (cItem.meta.itemStatus is CIStatus.RcvNew) {
|
||||
increaseUnreadCounter(rhId, currentUser.value!!)
|
||||
chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, unreadMentions = if (cItem.meta.userMention) chat.chatStats.unreadMentions + 1 else chat.chatStats.unreadMentions)
|
||||
}
|
||||
else
|
||||
chat.chatStats
|
||||
)
|
||||
updateChatTagReadNoContentTag(chats[i], wasUnread)
|
||||
|
||||
if (appPlatform.isDesktop && cItem.chatDir.sent) {
|
||||
reorderChat(chatsContext.chats[i], 0)
|
||||
reorderChat(chats[i], 0)
|
||||
} else {
|
||||
popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = i)
|
||||
}
|
||||
} else {
|
||||
if (cInfo.groupChatScope() == null) {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
} else {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = emptyList()))
|
||||
}
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
}
|
||||
// add to current scope
|
||||
withContext(Dispatchers.Main) {
|
||||
if (chatItemBelongsToScope(cInfo, cItem)) {
|
||||
// add to current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
// Prevent situation when chat item already in the list received from backend
|
||||
if (chatItems.value.none { it.id == cItem.id }) {
|
||||
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
|
@ -506,101 +476,84 @@ object ChatModel {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun chatItemBelongsToScope(cInfo: ChatInfo, cItem: ChatItem): Boolean =
|
||||
when (secondaryContextFilter) {
|
||||
null ->
|
||||
chatId.value == cInfo.id && cInfo.groupChatScope() == null
|
||||
is SecondaryContextFilter.GroupChatScopeContext -> {
|
||||
val cInfoScope = cInfo.groupChatScope()
|
||||
if (cInfoScope != null) {
|
||||
chatId.value == cInfo.id && sameChatScope(cInfoScope, secondaryContextFilter.groupScopeInfo.toChatScope())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
is SecondaryContextFilter.MsgContentTagContext ->
|
||||
chatId.value == cInfo.id && cItem.isReport
|
||||
}
|
||||
|
||||
suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean {
|
||||
var itemAdded = false
|
||||
// update chat list
|
||||
if (cInfo.groupChatScope() == null) {
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val pItem = chat.chatItems.lastOrNull()
|
||||
if (pItem?.id == cItem.id) {
|
||||
chats[i] = chat.copy(chatItems = arrayListOf(cItem))
|
||||
if (pItem.isRcvNew && !cItem.isRcvNew) {
|
||||
// status changed from New to Read, update counter
|
||||
decreaseCounterInPrimaryContext(rhId, cInfo.id)
|
||||
}
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
val res: Boolean
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val pItem = chat.chatItems.lastOrNull()
|
||||
if (pItem?.id == cItem.id) {
|
||||
chats[i] = chat.copy(chatItems = arrayListOf(cItem))
|
||||
if (pItem.isRcvNew && !cItem.isRcvNew) {
|
||||
// status changed from New to Read, update counter
|
||||
decreaseCounterInChatNoContentTag(rhId, cInfo.id)
|
||||
}
|
||||
} else {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
itemAdded = true
|
||||
}
|
||||
res = false
|
||||
} else {
|
||||
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||
res = true
|
||||
}
|
||||
// update current scope
|
||||
withContext(Dispatchers.Main) {
|
||||
if (chatItemBelongsToScope(cInfo, cItem)) {
|
||||
return withContext(Dispatchers.Main) {
|
||||
// update current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
if (cItem.isDeletedContent || cItem.meta.itemDeleted != null) {
|
||||
AudioPlayer.stop(cItem)
|
||||
}
|
||||
val items = chatItems.value
|
||||
val itemIndex = items.indexOfFirst { it.id == cItem.id }
|
||||
if (itemIndex >= 0) {
|
||||
val oldStatus = items[itemIndex].meta.itemStatus
|
||||
val newStatus = cItem.meta.itemStatus
|
||||
val ci = if (shouldKeepOldSndCIStatus(oldStatus, newStatus)) {
|
||||
cItem.copy(meta = cItem.meta.copy(itemStatus = oldStatus))
|
||||
items[itemIndex] = cItem
|
||||
false
|
||||
} else {
|
||||
val status = chatItemStatuses.remove(cItem.id)
|
||||
val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) {
|
||||
cItem.copy(meta = cItem.meta.copy(itemStatus = status))
|
||||
} else {
|
||||
cItem
|
||||
}
|
||||
items[itemIndex] = ci
|
||||
} else {
|
||||
addToChatItems(cItem)
|
||||
itemAdded = true
|
||||
addToChatItems(ci)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
res
|
||||
}
|
||||
}
|
||||
return itemAdded
|
||||
}
|
||||
|
||||
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null, atIndex: Int? = null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (chatItemBelongsToScope(cInfo, cItem)) {
|
||||
if (chatId.value == cInfo.id) {
|
||||
val items = chatItems.value
|
||||
val itemIndex = atIndex ?: items.indexOfFirst { it.id == cItem.id }
|
||||
if (itemIndex >= 0) {
|
||||
items[itemIndex] = cItem
|
||||
}
|
||||
} else if (status != null) {
|
||||
chatItemStatuses[cItem.id] = status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
|
||||
// update chat list
|
||||
if (cInfo.groupChatScope() == null) {
|
||||
if (cItem.isRcvNew) {
|
||||
decreaseCounterInPrimaryContext(rhId, cInfo.id)
|
||||
}
|
||||
// update preview
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val pItem = chat.chatItems.lastOrNull()
|
||||
if (pItem?.id == cItem.id) {
|
||||
chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy))
|
||||
}
|
||||
if (cItem.isRcvNew) {
|
||||
decreaseCounterInChatNoContentTag(rhId, cInfo.id)
|
||||
}
|
||||
// update previews
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
val chat: Chat
|
||||
if (i >= 0) {
|
||||
chat = chats[i]
|
||||
val pItem = chat.chatItems.lastOrNull()
|
||||
if (pItem?.id == cItem.id) {
|
||||
chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy))
|
||||
}
|
||||
}
|
||||
// remove from current scope
|
||||
if (chatItemBelongsToScope(cInfo, cItem)) {
|
||||
// remove from current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
chatItems.removeAllAndNotify {
|
||||
// We delete taking into account meta.createdAt to make sure we will not be in situation when two items with the same id will be deleted
|
||||
// (it can happen if already deleted chat item in backend still in the list and new one came with the same (re-used) chat item id)
|
||||
|
@ -633,7 +586,7 @@ object ChatModel {
|
|||
Log.d(TAG, "exiting removeMemberItems")
|
||||
return
|
||||
}
|
||||
val cInfo = ChatInfo.Group(groupInfo, groupChatScope = null) // TODO [knocking] review
|
||||
val cInfo = ChatInfo.Group(groupInfo)
|
||||
if (chatId.value == groupInfo.id) {
|
||||
for (i in 0 until chatItems.value.size) {
|
||||
val updatedItem = removedUpdatedItem(chatItems.value[i])
|
||||
|
@ -664,6 +617,7 @@ object ChatModel {
|
|||
}
|
||||
// clear current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
chatItemStatuses.clear()
|
||||
chatItems.clearAndNotify()
|
||||
}
|
||||
}
|
||||
|
@ -734,7 +688,7 @@ object ChatModel {
|
|||
chats[chatIdx] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(unreadCount = unreadCount, unreadMentions = unreadMentions)
|
||||
)
|
||||
updateChatTagReadInPrimaryContext(chats[chatIdx], wasUnread)
|
||||
updateChatTagReadNoContentTag(chats[chatIdx], wasUnread)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -775,9 +729,9 @@ object ChatModel {
|
|||
return markedRead to mentionsMarkedRead
|
||||
}
|
||||
|
||||
private fun decreaseCounterInPrimaryContext(rhId: Long?, chatId: ChatId) {
|
||||
private fun decreaseCounterInChatNoContentTag(rhId: Long?, chatId: ChatId) {
|
||||
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
|
||||
if (secondaryContextFilter != null) return
|
||||
if (contentTag != null) return
|
||||
|
||||
val chatIndex = getChatIndex(rhId, chatId)
|
||||
if (chatIndex == -1) return
|
||||
|
@ -791,7 +745,7 @@ object ChatModel {
|
|||
unreadCount = unreadCount,
|
||||
)
|
||||
)
|
||||
updateChatTagReadInPrimaryContext(chats[chatIndex], wasUnread)
|
||||
updateChatTagReadNoContentTag(chats[chatIndex], wasUnread)
|
||||
}
|
||||
|
||||
fun removeChat(rhId: Long?, id: String) {
|
||||
|
@ -860,16 +814,16 @@ object ChatModel {
|
|||
}
|
||||
|
||||
fun increaseUnreadCounter(rhId: Long?, user: UserLike) {
|
||||
changeUnreadCounterInPrimaryContext(rhId, user, 1)
|
||||
changeUnreadCounterNoContentTag(rhId, user, 1)
|
||||
}
|
||||
|
||||
fun decreaseUnreadCounter(rhId: Long?, user: UserLike, by: Int = 1) {
|
||||
changeUnreadCounterInPrimaryContext(rhId, user, -by)
|
||||
changeUnreadCounterNoContentTag(rhId, user, -by)
|
||||
}
|
||||
|
||||
private fun changeUnreadCounterInPrimaryContext(rhId: Long?, user: UserLike, by: Int) {
|
||||
private fun changeUnreadCounterNoContentTag(rhId: Long?, user: UserLike, by: Int) {
|
||||
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
|
||||
if (secondaryContextFilter != null) return
|
||||
if (contentTag != null) return
|
||||
|
||||
val i = users.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == rhId }
|
||||
if (i != -1) {
|
||||
|
@ -877,9 +831,9 @@ object ChatModel {
|
|||
}
|
||||
}
|
||||
|
||||
fun updateChatTagReadInPrimaryContext(chat: Chat, wasUnread: Boolean) {
|
||||
fun updateChatTagReadNoContentTag(chat: Chat, wasUnread: Boolean) {
|
||||
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
|
||||
if (secondaryContextFilter != null) return
|
||||
if (contentTag != null) return
|
||||
|
||||
val tags = chat.chatInfo.chatTags ?: return
|
||||
val nowUnread = chat.unreadTag
|
||||
|
@ -889,21 +843,21 @@ object ChatModel {
|
|||
unreadTags[tag] = (unreadTags[tag] ?: 0) + 1
|
||||
}
|
||||
} else if (!nowUnread && wasUnread) {
|
||||
markChatTagReadInPrimaryContext_(chat, tags)
|
||||
markChatTagReadNoContentTag_(chat, tags)
|
||||
}
|
||||
}
|
||||
|
||||
fun markChatTagRead(chat: Chat) {
|
||||
if (chat.unreadTag) {
|
||||
chat.chatInfo.chatTags?.let { tags ->
|
||||
markChatTagReadInPrimaryContext_(chat, tags)
|
||||
markChatTagReadNoContentTag_(chat, tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun markChatTagReadInPrimaryContext_(chat: Chat, tags: List<Long>) {
|
||||
private fun markChatTagReadNoContentTag_(chat: Chat, tags: List<Long>) {
|
||||
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
|
||||
if (secondaryContextFilter != null) return
|
||||
if (contentTag != null) return
|
||||
|
||||
for (tag in tags) {
|
||||
val count = unreadTags[tag]
|
||||
|
@ -935,12 +889,12 @@ object ChatModel {
|
|||
val wasReportsCount = chat.chatStats.reportsCount
|
||||
val nowReportsCount = chats[i].chatStats.reportsCount
|
||||
val by = if (wasReportsCount == 0 && nowReportsCount > 0) 1 else if (wasReportsCount > 0 && nowReportsCount == 0) -1 else 0
|
||||
changeGroupReportsTagInPrimaryContext(by)
|
||||
changeGroupReportsTagNoContentTag(by)
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeGroupReportsTagInPrimaryContext(by: Int = 0) {
|
||||
if (by == 0 || secondaryContextFilter != null) return
|
||||
private fun changeGroupReportsTagNoContentTag(by: Int = 0) {
|
||||
if (by == 0 || contentTag != null) return
|
||||
presetTags[PresetTagKind.GROUP_REPORTS] = kotlin.math.max(0, (presetTags[PresetTagKind.GROUP_REPORTS] ?: 0) + by)
|
||||
clearActiveChatFilterIfNeeded()
|
||||
}
|
||||
|
@ -1145,28 +1099,6 @@ enum class ChatType(val type: String) {
|
|||
ContactConnection(":");
|
||||
}
|
||||
|
||||
sealed class GroupChatScope {
|
||||
class MemberSupport(val groupMemberId_: Long?): GroupChatScope()
|
||||
}
|
||||
|
||||
fun sameChatScope(scope1: GroupChatScope, scope2: GroupChatScope) =
|
||||
scope1 is GroupChatScope.MemberSupport
|
||||
&& scope2 is GroupChatScope.MemberSupport
|
||||
&& scope1.groupMemberId_ == scope2.groupMemberId_
|
||||
|
||||
@Serializable
|
||||
sealed class GroupChatScopeInfo {
|
||||
@Serializable @SerialName("memberSupport") data class MemberSupport(val groupMember_: GroupMember?) : GroupChatScopeInfo()
|
||||
|
||||
fun toChatScope(): GroupChatScope =
|
||||
when (this) {
|
||||
is MemberSupport -> when (groupMember_) {
|
||||
null -> GroupChatScope.MemberSupport(groupMemberId_ = null)
|
||||
else -> GroupChatScope.MemberSupport(groupMemberId_ = groupMember_.groupMemberId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class User(
|
||||
val remoteHostId: Long?,
|
||||
|
@ -1272,6 +1204,7 @@ interface SomeChat {
|
|||
val apiId: Long
|
||||
val ready: Boolean
|
||||
val chatDeleted: Boolean
|
||||
val userCantSendReason: Pair<String, String?>?
|
||||
val sendMsgEnabled: Boolean
|
||||
val incognito: Boolean
|
||||
fun featureEnabled(feature: ChatFeature): Boolean
|
||||
|
@ -1296,19 +1229,6 @@ data class Chat(
|
|||
else -> false
|
||||
}
|
||||
|
||||
val userIsObserver: Boolean get() = when(chatInfo) {
|
||||
is ChatInfo.Group -> {
|
||||
val m = chatInfo.groupInfo.membership
|
||||
m.memberActive && m.memberRole == GroupMemberRole.Observer
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
val userIsPending: Boolean get() = when(chatInfo) {
|
||||
is ChatInfo.Group -> chatInfo.groupInfo.membership.memberPending
|
||||
else -> false
|
||||
}
|
||||
|
||||
val unreadTag: Boolean get() = when (chatInfo.chatSettings?.enableNtfs) {
|
||||
All -> chatStats.unreadChat || chatStats.unreadCount > 0
|
||||
Mentions -> chatStats.unreadChat || chatStats.unreadMentions > 0
|
||||
|
@ -1317,16 +1237,6 @@ data class Chat(
|
|||
|
||||
val id: String get() = chatInfo.id
|
||||
|
||||
val supportUnreadCount: Int get() = when (chatInfo) {
|
||||
is ChatInfo.Group ->
|
||||
if (chatInfo.groupInfo.canModerate) {
|
||||
chatInfo.groupInfo.membersRequireAttention
|
||||
} else {
|
||||
chatInfo.groupInfo.membership.supportChat?.unread ?: 0
|
||||
}
|
||||
else -> 0
|
||||
}
|
||||
|
||||
fun groupFeatureEnabled(feature: GroupFeature): Boolean =
|
||||
if (chatInfo is ChatInfo.Group) {
|
||||
chatInfo.groupInfo.groupFeatureEnabled(feature)
|
||||
|
@ -1365,6 +1275,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||
override val apiId get() = contact.apiId
|
||||
override val ready get() = contact.ready
|
||||
override val chatDeleted get() = contact.chatDeleted
|
||||
override val userCantSendReason get() = contact.userCantSendReason
|
||||
override val sendMsgEnabled get() = contact.sendMsgEnabled
|
||||
override val incognito get() = contact.incognito
|
||||
override fun featureEnabled(feature: ChatFeature) = contact.featureEnabled(feature)
|
||||
|
@ -1383,13 +1294,14 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||
}
|
||||
|
||||
@Serializable @SerialName("group")
|
||||
data class Group(val groupInfo: GroupInfo, val groupChatScope: GroupChatScopeInfo?): ChatInfo() {
|
||||
data class Group(val groupInfo: GroupInfo): ChatInfo() {
|
||||
override val chatType get() = ChatType.Group
|
||||
override val localDisplayName get() = groupInfo.localDisplayName
|
||||
override val id get() = groupInfo.id
|
||||
override val apiId get() = groupInfo.apiId
|
||||
override val ready get() = groupInfo.ready
|
||||
override val chatDeleted get() = groupInfo.chatDeleted
|
||||
override val userCantSendReason get() = groupInfo.userCantSendReason
|
||||
override val sendMsgEnabled get() = groupInfo.sendMsgEnabled
|
||||
override val incognito get() = groupInfo.incognito
|
||||
override fun featureEnabled(feature: ChatFeature) = groupInfo.featureEnabled(feature)
|
||||
|
@ -1402,7 +1314,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||
override val localAlias get() = groupInfo.localAlias
|
||||
|
||||
companion object {
|
||||
val sampleData = Group(GroupInfo.sampleData, groupChatScope = null)
|
||||
val sampleData = Group(GroupInfo.sampleData)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1414,6 +1326,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||
override val apiId get() = noteFolder.apiId
|
||||
override val ready get() = noteFolder.ready
|
||||
override val chatDeleted get() = noteFolder.chatDeleted
|
||||
override val userCantSendReason get() = noteFolder.userCantSendReason
|
||||
override val sendMsgEnabled get() = noteFolder.sendMsgEnabled
|
||||
override val incognito get() = noteFolder.incognito
|
||||
override fun featureEnabled(feature: ChatFeature) = noteFolder.featureEnabled(feature)
|
||||
|
@ -1438,6 +1351,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||
override val apiId get() = contactRequest.apiId
|
||||
override val ready get() = contactRequest.ready
|
||||
override val chatDeleted get() = contactRequest.chatDeleted
|
||||
override val userCantSendReason get() = contactRequest.userCantSendReason
|
||||
override val sendMsgEnabled get() = contactRequest.sendMsgEnabled
|
||||
override val incognito get() = contactRequest.incognito
|
||||
override fun featureEnabled(feature: ChatFeature) = contactRequest.featureEnabled(feature)
|
||||
|
@ -1462,6 +1376,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||
override val apiId get() = contactConnection.apiId
|
||||
override val ready get() = contactConnection.ready
|
||||
override val chatDeleted get() = contactConnection.chatDeleted
|
||||
override val userCantSendReason get() = contactConnection.userCantSendReason
|
||||
override val sendMsgEnabled get() = contactConnection.sendMsgEnabled
|
||||
override val incognito get() = contactConnection.incognito
|
||||
override fun featureEnabled(feature: ChatFeature) = contactConnection.featureEnabled(feature)
|
||||
|
@ -1491,6 +1406,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||
override val id get() = "?$apiId"
|
||||
override val ready get() = false
|
||||
override val chatDeleted get() = false
|
||||
override val userCantSendReason get() = generalGetString(MR.strings.cant_send_message_generic) to null
|
||||
override val sendMsgEnabled get() = false
|
||||
override val incognito get() = false
|
||||
override fun featureEnabled(feature: ChatFeature) = false
|
||||
|
@ -1506,11 +1422,6 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||
}
|
||||
}
|
||||
|
||||
fun groupChatScope(): GroupChatScope? = when (this) {
|
||||
is Group -> groupChatScope?.toChatScope()
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun ntfsEnabled(ci: ChatItem): Boolean =
|
||||
ntfsEnabled(ci.meta.userMention)
|
||||
|
||||
|
@ -1538,16 +1449,6 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||
is InvalidJSON -> updatedAt
|
||||
}
|
||||
|
||||
val userCanSend: Boolean
|
||||
get() = when (this) {
|
||||
is ChatInfo.Direct -> true
|
||||
is ChatInfo.Group ->
|
||||
(groupInfo.membership.memberRole >= GroupMemberRole.Member && !groupInfo.membership.memberPending)
|
||||
|| groupChatScope != null
|
||||
is ChatInfo.Local -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val chatTags: List<Long>?
|
||||
get() = when (this) {
|
||||
is Direct -> contact.chatTags
|
||||
|
@ -1564,13 +1465,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||
is Direct -> contact.activeConn == null && contact.profile.contactLink != null && contact.active
|
||||
else -> false
|
||||
}
|
||||
|
||||
val groupInfo_: GroupInfo?
|
||||
get() = when (this) {
|
||||
is Group -> groupInfo
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class NetworkStatus {
|
||||
|
@ -1624,13 +1519,17 @@ data class Contact(
|
|||
override val ready get() = activeConn?.connStatus == ConnStatus.Ready
|
||||
val sndReady get() = ready || activeConn?.connStatus == ConnStatus.SndReady
|
||||
val active get() = contactStatus == ContactStatus.Active
|
||||
override val sendMsgEnabled get() = (
|
||||
sndReady
|
||||
&& active
|
||||
&& !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?: false)
|
||||
&& !(activeConn?.connDisabled ?: true)
|
||||
)
|
||||
|| nextSendGrpInv
|
||||
override val userCantSendReason: Pair<String, String?>?
|
||||
get() {
|
||||
// TODO [short links] this will have additional statuses for pending contact requests before they are accepted
|
||||
if (nextSendGrpInv) return null
|
||||
if (!active) return generalGetString(MR.strings.cant_send_message_contact_deleted) to null
|
||||
if (!sndReady) return generalGetString(MR.strings.cant_send_message_contact_not_ready) to null
|
||||
if (activeConn?.connectionStats?.ratchetSyncSendProhibited == true) return generalGetString(MR.strings.cant_send_message_contact_not_synchronized) to null
|
||||
if (activeConn?.connDisabled == true) return generalGetString(MR.strings.cant_send_message_contact_disabled) to null
|
||||
return null
|
||||
}
|
||||
override val sendMsgEnabled get() = userCantSendReason == null
|
||||
val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent
|
||||
override val incognito get() = contactConnIncognito
|
||||
override fun featureEnabled(feature: ChatFeature) = when (feature) {
|
||||
|
@ -1855,7 +1754,6 @@ data class GroupInfo (
|
|||
override val updatedAt: Instant,
|
||||
val chatTs: Instant?,
|
||||
val uiThemes: ThemeModeOverrides? = null,
|
||||
val membersRequireAttention: Int,
|
||||
val chatTags: List<Long>,
|
||||
val chatItemTTL: Long?,
|
||||
override val localAlias: String,
|
||||
|
@ -1865,7 +1763,23 @@ data class GroupInfo (
|
|||
override val apiId get() = groupId
|
||||
override val ready get() = membership.memberActive
|
||||
override val chatDeleted get() = false
|
||||
override val sendMsgEnabled get() = membership.memberActive
|
||||
override val userCantSendReason: Pair<String, String?>? get() =
|
||||
if (membership.memberActive) {
|
||||
if (membership.memberRole == GroupMemberRole.Observer) {
|
||||
generalGetString(MR.strings.observer_cant_send_message_title) to generalGetString(MR.strings.observer_cant_send_message_desc)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
when (membership.memberStatus) {
|
||||
GroupMemberStatus.MemRejected -> generalGetString(MR.strings.cant_send_message_rejected) to null
|
||||
GroupMemberStatus.MemGroupDeleted -> generalGetString(MR.strings.cant_send_message_group_deleted) to null
|
||||
GroupMemberStatus.MemRemoved -> generalGetString(MR.strings.cant_send_message_mem_removed) to null
|
||||
GroupMemberStatus.MemLeft -> generalGetString(MR.strings.cant_send_message_you_left) to null
|
||||
else -> generalGetString(MR.strings.cant_send_message_generic) to null
|
||||
}
|
||||
}
|
||||
override val sendMsgEnabled get() = userCantSendReason == null
|
||||
override val incognito get() = membership.memberIncognito
|
||||
override fun featureEnabled(feature: ChatFeature) = when (feature) {
|
||||
ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on
|
||||
|
@ -1883,7 +1797,7 @@ data class GroupInfo (
|
|||
get() = membership.memberRole == GroupMemberRole.Owner && membership.memberCurrent
|
||||
|
||||
val canDelete: Boolean
|
||||
get() = membership.memberRole == GroupMemberRole.Owner || !membership.memberCurrentOrPending
|
||||
get() = membership.memberRole == GroupMemberRole.Owner || !membership.memberCurrent
|
||||
|
||||
val canAddMembers: Boolean
|
||||
get() = membership.memberRole >= GroupMemberRole.Admin && membership.memberActive
|
||||
|
@ -1918,7 +1832,6 @@ data class GroupInfo (
|
|||
updatedAt = Clock.System.now(),
|
||||
chatTs = Clock.System.now(),
|
||||
uiThemes = null,
|
||||
membersRequireAttention = 0,
|
||||
chatTags = emptyList(),
|
||||
localAlias = "",
|
||||
chatItemTTL = null
|
||||
|
@ -1936,8 +1849,7 @@ data class GroupProfile (
|
|||
val description: String? = null,
|
||||
override val image: String? = null,
|
||||
override val localAlias: String = "",
|
||||
val groupPreferences: GroupPreferences? = null,
|
||||
val memberAdmission: GroupMemberAdmission? = null
|
||||
val groupPreferences: GroupPreferences? = null
|
||||
): NamedChat {
|
||||
companion object {
|
||||
val sampleData = GroupProfile(
|
||||
|
@ -1947,27 +1859,6 @@ data class GroupProfile (
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GroupMemberAdmission(
|
||||
val review: MemberCriteria? = null,
|
||||
) {
|
||||
companion object {
|
||||
val sampleData = GroupMemberAdmission(
|
||||
review = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class MemberCriteria {
|
||||
@SerialName("all") All;
|
||||
|
||||
val text: String
|
||||
get() = when(this) {
|
||||
MemberCriteria.All -> generalGetString(MR.strings.member_criteria_all)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class BusinessChatInfo (
|
||||
val chatType: BusinessChatType,
|
||||
|
@ -1996,8 +1887,7 @@ data class GroupMember (
|
|||
val memberProfile: LocalProfile,
|
||||
val memberContactId: Long? = null,
|
||||
val memberContactProfileId: Long,
|
||||
var activeConn: Connection? = null,
|
||||
val supportChat: GroupSupportChat? = null
|
||||
var activeConn: Connection? = null
|
||||
): NamedChat {
|
||||
val id: String get() = "#$groupId @$groupMemberId"
|
||||
val ready get() = activeConn?.connStatus == ConnStatus.Ready
|
||||
|
@ -2056,7 +1946,6 @@ data class GroupMember (
|
|||
GroupMemberStatus.MemUnknown -> false
|
||||
GroupMemberStatus.MemInvited -> false
|
||||
GroupMemberStatus.MemPendingApproval -> true
|
||||
GroupMemberStatus.MemPendingReview -> true
|
||||
GroupMemberStatus.MemIntroduced -> false
|
||||
GroupMemberStatus.MemIntroInvited -> false
|
||||
GroupMemberStatus.MemAccepted -> false
|
||||
|
@ -2074,7 +1963,6 @@ data class GroupMember (
|
|||
GroupMemberStatus.MemUnknown -> false
|
||||
GroupMemberStatus.MemInvited -> false
|
||||
GroupMemberStatus.MemPendingApproval -> false
|
||||
GroupMemberStatus.MemPendingReview -> false
|
||||
GroupMemberStatus.MemIntroduced -> true
|
||||
GroupMemberStatus.MemIntroInvited -> true
|
||||
GroupMemberStatus.MemAccepted -> true
|
||||
|
@ -2084,15 +1972,6 @@ data class GroupMember (
|
|||
GroupMemberStatus.MemCreator -> true
|
||||
}
|
||||
|
||||
val memberPending: Boolean get() = when (this.memberStatus) {
|
||||
GroupMemberStatus.MemPendingApproval -> true
|
||||
GroupMemberStatus.MemPendingReview -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
val memberCurrentOrPending: Boolean get() =
|
||||
memberCurrent || memberPending
|
||||
|
||||
fun canBeRemoved(groupInfo: GroupInfo): Boolean {
|
||||
val userRole = groupInfo.membership.memberRole
|
||||
return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft
|
||||
|
@ -2133,14 +2012,6 @@ data class GroupMember (
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class GroupSupportChat (
|
||||
val chatTs: Instant,
|
||||
val unread: Int,
|
||||
val memberAttention: Int,
|
||||
val mentions: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GroupMemberSettings(val showMessages: Boolean) {}
|
||||
|
||||
|
@ -2197,7 +2068,6 @@ enum class GroupMemberStatus {
|
|||
@SerialName("unknown") MemUnknown,
|
||||
@SerialName("invited") MemInvited,
|
||||
@SerialName("pending_approval") MemPendingApproval,
|
||||
@SerialName("pending_review") MemPendingReview,
|
||||
@SerialName("introduced") MemIntroduced,
|
||||
@SerialName("intro-inv") MemIntroInvited,
|
||||
@SerialName("accepted") MemAccepted,
|
||||
|
@ -2214,7 +2084,6 @@ enum class GroupMemberStatus {
|
|||
MemUnknown -> generalGetString(MR.strings.group_member_status_unknown)
|
||||
MemInvited -> generalGetString(MR.strings.group_member_status_invited)
|
||||
MemPendingApproval -> generalGetString(MR.strings.group_member_status_pending_approval)
|
||||
MemPendingReview -> generalGetString(MR.strings.group_member_status_pending_review)
|
||||
MemIntroduced -> generalGetString(MR.strings.group_member_status_introduced)
|
||||
MemIntroInvited -> generalGetString(MR.strings.group_member_status_intro_invitation)
|
||||
MemAccepted -> generalGetString(MR.strings.group_member_status_accepted)
|
||||
|
@ -2232,7 +2101,6 @@ enum class GroupMemberStatus {
|
|||
MemUnknown -> generalGetString(MR.strings.group_member_status_unknown_short)
|
||||
MemInvited -> generalGetString(MR.strings.group_member_status_invited)
|
||||
MemPendingApproval -> generalGetString(MR.strings.group_member_status_pending_approval_short)
|
||||
MemPendingReview -> generalGetString(MR.strings.group_member_status_pending_review_short)
|
||||
MemIntroduced -> generalGetString(MR.strings.group_member_status_connecting)
|
||||
MemIntroInvited -> generalGetString(MR.strings.group_member_status_connecting)
|
||||
MemAccepted -> generalGetString(MR.strings.group_member_status_connecting)
|
||||
|
@ -2287,6 +2155,7 @@ class NoteFolder(
|
|||
override val apiId get() = noteFolderId
|
||||
override val chatDeleted get() = false
|
||||
override val ready get() = true
|
||||
override val userCantSendReason: Pair<String, String?>? = null
|
||||
override val sendMsgEnabled get() = true
|
||||
override val incognito get() = false
|
||||
override fun featureEnabled(feature: ChatFeature) = feature == ChatFeature.Voice
|
||||
|
@ -2323,6 +2192,7 @@ class UserContactRequest (
|
|||
override val apiId get() = contactRequestId
|
||||
override val chatDeleted get() = false
|
||||
override val ready get() = true
|
||||
override val userCantSendReason = generalGetString(MR.strings.cant_send_message_generic) to null
|
||||
override val sendMsgEnabled get() = false
|
||||
override val incognito get() = false
|
||||
override fun featureEnabled(feature: ChatFeature) = false
|
||||
|
@ -2362,6 +2232,7 @@ class PendingContactConnection(
|
|||
override val apiId get() = pccConnId
|
||||
override val chatDeleted get() = false
|
||||
override val ready get() = false
|
||||
override val userCantSendReason = generalGetString(MR.strings.cant_send_message_generic) to null
|
||||
override val sendMsgEnabled get() = false
|
||||
override val incognito get() = customUserProfileId != null
|
||||
override fun featureEnabled(feature: ChatFeature) = false
|
||||
|
@ -2580,16 +2451,11 @@ data class ChatItem (
|
|||
is CIContent.RcvGroupFeature,
|
||||
is CIContent.SndGroupFeature -> CIMergeCategory.ChatFeature
|
||||
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
|
||||
is RcvGroupEvent.UserRole,
|
||||
is RcvGroupEvent.UserDeleted,
|
||||
is RcvGroupEvent.GroupDeleted,
|
||||
is RcvGroupEvent.MemberCreatedContact,
|
||||
is RcvGroupEvent.NewMemberPendingReview ->
|
||||
null
|
||||
is RcvGroupEvent.UserRole, is RcvGroupEvent.UserDeleted, is RcvGroupEvent.GroupDeleted, is RcvGroupEvent.MemberCreatedContact -> null
|
||||
else -> CIMergeCategory.RcvGroupEvent
|
||||
}
|
||||
is CIContent.SndGroupEventContent -> when (content.sndGroupEvent) {
|
||||
is SndGroupEvent.UserRole, is SndGroupEvent.UserLeft, is SndGroupEvent.MemberAccepted, is SndGroupEvent.UserPendingReview -> null
|
||||
is SndGroupEvent.UserRole, is SndGroupEvent.UserLeft -> null
|
||||
else -> CIMergeCategory.SndGroupEvent
|
||||
}
|
||||
else -> {
|
||||
|
@ -2660,8 +2526,6 @@ data class ChatItem (
|
|||
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
|
||||
is RcvGroupEvent.MemberAdded -> false
|
||||
is RcvGroupEvent.MemberConnected -> false
|
||||
is RcvGroupEvent.MemberAccepted -> false
|
||||
is RcvGroupEvent.UserAccepted -> false
|
||||
is RcvGroupEvent.MemberLeft -> false
|
||||
is RcvGroupEvent.MemberRole -> false
|
||||
is RcvGroupEvent.MemberBlocked -> false
|
||||
|
@ -2673,7 +2537,6 @@ data class ChatItem (
|
|||
is RcvGroupEvent.InvitedViaGroupLink -> false
|
||||
is RcvGroupEvent.MemberCreatedContact -> false
|
||||
is RcvGroupEvent.MemberProfileUpdated -> false
|
||||
is RcvGroupEvent.NewMemberPendingReview -> true
|
||||
}
|
||||
is CIContent.SndGroupEventContent -> false
|
||||
is CIContent.RcvConnEventContent -> false
|
||||
|
@ -2855,11 +2718,6 @@ data class ChatItem (
|
|||
}
|
||||
}
|
||||
|
||||
sealed class SecondaryContextFilter {
|
||||
class GroupChatScopeContext(val groupScopeInfo: GroupChatScopeInfo): SecondaryContextFilter()
|
||||
class MsgContentTagContext(val contentTag: MsgContentTag): SecondaryContextFilter()
|
||||
}
|
||||
|
||||
fun MutableState<SnapshotStateList<Chat>>.add(index: Int, elem: Chat) {
|
||||
value = SnapshotStateList<Chat>().apply { addAll(value); add(index, elem) }
|
||||
}
|
||||
|
@ -3118,19 +2976,6 @@ 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
|
||||
is SndRcvd -> false
|
||||
is SndErrorAuth -> true
|
||||
is CISSndError -> true
|
||||
is SndWarning -> true
|
||||
is RcvNew -> false
|
||||
is RcvRead -> false
|
||||
is Invalid -> false
|
||||
}
|
||||
|
||||
fun statusIcon(
|
||||
primaryColor: Color,
|
||||
metaColor: Color = CurrentColors.value.colors.secondary,
|
||||
|
@ -3170,13 +3015,6 @@ sealed class CIStatus {
|
|||
}
|
||||
}
|
||||
|
||||
fun shouldKeepOldSndCIStatus(oldStatus: CIStatus, newStatus: CIStatus): Boolean =
|
||||
when {
|
||||
oldStatus is CIStatus.SndRcvd && newStatus !is CIStatus.SndRcvd -> true
|
||||
oldStatus.isSent() && newStatus is CIStatus.SndNew -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class SndError {
|
||||
@Serializable @SerialName("auth") class Auth: SndError()
|
||||
|
@ -4315,8 +4153,6 @@ sealed class RcvDirectEvent() {
|
|||
sealed class RcvGroupEvent() {
|
||||
@Serializable @SerialName("memberAdded") class MemberAdded(val groupMemberId: Long, val profile: Profile): RcvGroupEvent()
|
||||
@Serializable @SerialName("memberConnected") class MemberConnected(): RcvGroupEvent()
|
||||
@Serializable @SerialName("memberAccepted") class MemberAccepted(val groupMemberId: Long, val profile: Profile): RcvGroupEvent()
|
||||
@Serializable @SerialName("userAccepted") class UserAccepted(): RcvGroupEvent()
|
||||
@Serializable @SerialName("memberLeft") class MemberLeft(): RcvGroupEvent()
|
||||
@Serializable @SerialName("memberRole") class MemberRole(val groupMemberId: Long, val profile: Profile, val role: GroupMemberRole): RcvGroupEvent()
|
||||
@Serializable @SerialName("memberBlocked") class MemberBlocked(val groupMemberId: Long, val profile: Profile, val blocked: Boolean): RcvGroupEvent()
|
||||
|
@ -4328,13 +4164,10 @@ sealed class RcvGroupEvent() {
|
|||
@Serializable @SerialName("invitedViaGroupLink") class InvitedViaGroupLink(): RcvGroupEvent()
|
||||
@Serializable @SerialName("memberCreatedContact") class MemberCreatedContact(): RcvGroupEvent()
|
||||
@Serializable @SerialName("memberProfileUpdated") class MemberProfileUpdated(val fromProfile: Profile, val toProfile: Profile): RcvGroupEvent()
|
||||
@Serializable @SerialName("newMemberPendingReview") class NewMemberPendingReview(): RcvGroupEvent()
|
||||
|
||||
val text: String get() = when (this) {
|
||||
is MemberAdded -> String.format(generalGetString(MR.strings.rcv_group_event_member_added), profile.profileViewName)
|
||||
is MemberConnected -> generalGetString(MR.strings.rcv_group_event_member_connected)
|
||||
is MemberAccepted -> String.format(generalGetString(MR.strings.rcv_group_event_member_accepted), profile.profileViewName)
|
||||
is UserAccepted -> generalGetString(MR.strings.rcv_group_event_user_accepted)
|
||||
is MemberLeft -> generalGetString(MR.strings.rcv_group_event_member_left)
|
||||
is MemberRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_member_role), profile.profileViewName, role.text)
|
||||
is MemberBlocked -> if (blocked) {
|
||||
|
@ -4350,7 +4183,6 @@ sealed class RcvGroupEvent() {
|
|||
is InvitedViaGroupLink -> generalGetString(MR.strings.rcv_group_event_invited_via_your_group_link)
|
||||
is MemberCreatedContact -> generalGetString(MR.strings.rcv_group_event_member_created_contact)
|
||||
is MemberProfileUpdated -> profileUpdatedText(fromProfile, toProfile)
|
||||
is NewMemberPendingReview -> generalGetString(MR.strings.rcv_group_event_new_member_pending_review)
|
||||
}
|
||||
|
||||
private fun profileUpdatedText(from: Profile, to: Profile): String =
|
||||
|
@ -4375,8 +4207,6 @@ sealed class SndGroupEvent() {
|
|||
@Serializable @SerialName("memberDeleted") class MemberDeleted(val groupMemberId: Long, val profile: Profile): SndGroupEvent()
|
||||
@Serializable @SerialName("userLeft") class UserLeft(): SndGroupEvent()
|
||||
@Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): SndGroupEvent()
|
||||
@Serializable @SerialName("memberAccepted") class MemberAccepted(val groupMemberId: Long, val profile: Profile): SndGroupEvent()
|
||||
@Serializable @SerialName("userPendingReview") class UserPendingReview(): SndGroupEvent()
|
||||
|
||||
val text: String get() = when (this) {
|
||||
is MemberRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_member_role), profile.profileViewName, role.text)
|
||||
|
@ -4389,8 +4219,6 @@ sealed class SndGroupEvent() {
|
|||
is MemberDeleted -> String.format(generalGetString(MR.strings.snd_group_event_member_deleted), profile.profileViewName)
|
||||
is UserLeft -> generalGetString(MR.strings.snd_group_event_user_left)
|
||||
is GroupUpdated -> generalGetString(MR.strings.snd_group_event_group_profile_updated)
|
||||
is MemberAccepted -> generalGetString(MR.strings.snd_group_event_member_accepted)
|
||||
is UserPendingReview -> generalGetString(MR.strings.snd_group_event_user_pending_review)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -163,7 +163,6 @@ class AppPreferences {
|
|||
val oneHandUICardShown = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN, false)
|
||||
val addressCreationCardShown = mkBoolPreference(SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN, false)
|
||||
val showMuteProfileAlert = mkBoolPreference(SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT, true)
|
||||
val showReportsInSupportChatAlert = mkBoolPreference(SHARED_PREFS_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT, true)
|
||||
val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null)
|
||||
val appUpdateChannel = mkEnumPreference(SHARED_PREFS_APP_UPDATE_CHANNEL, AppUpdatesChannel.DISABLED) { AppUpdatesChannel.entries.firstOrNull { it.name == this } }
|
||||
val appSkippedUpdate = mkStrPreference(SHARED_PREFS_APP_SKIPPED_UPDATE, "")
|
||||
|
@ -246,7 +245,6 @@ class AppPreferences {
|
|||
liveMessageAlertShown to false,
|
||||
showHiddenProfilesNotice to true,
|
||||
showMuteProfileAlert to true,
|
||||
showReportsInSupportChatAlert to true,
|
||||
showDeleteConversationNotice to true,
|
||||
showDeleteContactNotice to true,
|
||||
)
|
||||
|
@ -417,7 +415,6 @@ class AppPreferences {
|
|||
private const val SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN = "OneHandUICardShown"
|
||||
private const val SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN = "AddressCreationCardShown"
|
||||
private const val SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT = "ShowMuteProfileAlert"
|
||||
private const val SHARED_PREFS_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT = "ShowReportsInSupportChatAlert"
|
||||
private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase"
|
||||
private const val SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE = "InitialRandomDBPassphrase"
|
||||
private const val SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE = "EncryptedDBPassphrase"
|
||||
|
@ -885,8 +882,8 @@ object ChatController {
|
|||
return null
|
||||
}
|
||||
|
||||
suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, contentTag: MsgContentTag? = null, pagination: ChatPagination, search: String = ""): Pair<Chat, NavigationInfo>? {
|
||||
val r = sendCmd(rh, CC.ApiGetChat(type, id, scope, contentTag, pagination, search))
|
||||
suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, contentTag: MsgContentTag? = null, pagination: ChatPagination, search: String = ""): Pair<Chat, NavigationInfo>? {
|
||||
val r = sendCmd(rh, CC.ApiGetChat(type, id, contentTag, pagination, search))
|
||||
if (r is API.Result && r.res is CR.ApiChat) return if (rh == null) r.res.chat to r.res.navInfo else r.res.chat.copy(remoteHostId = rh) to r.res.navInfo
|
||||
Log.e(TAG, "apiGetChat bad response: ${r.responseType} ${r.details}")
|
||||
val e = (r as? API.Error)?.err
|
||||
|
@ -920,8 +917,8 @@ object ChatController {
|
|||
|
||||
suspend fun apiReorderChatTags(rh: Long?, tagIds: List<Long>) = sendCommandOkResp(rh, CC.ApiReorderChatTags(tagIds))
|
||||
|
||||
suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, live: Boolean = false, ttl: Int? = null, composedMessages: List<ComposedMessage>): List<AChatItem>? {
|
||||
val cmd = CC.ApiSendMessages(type, id, scope, live, ttl, composedMessages)
|
||||
suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, live: Boolean = false, ttl: Int? = null, composedMessages: List<ComposedMessage>): List<AChatItem>? {
|
||||
val cmd = CC.ApiSendMessages(type, id, live, ttl, composedMessages)
|
||||
return processSendMessageCmd(rh, cmd)
|
||||
}
|
||||
|
||||
|
@ -972,27 +969,27 @@ object ChatController {
|
|||
return null
|
||||
}
|
||||
|
||||
suspend fun apiGetChatItemInfo(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemId: Long): ChatItemInfo? {
|
||||
val r = sendCmd(rh, CC.ApiGetChatItemInfo(type, id, scope, itemId))
|
||||
suspend fun apiGetChatItemInfo(rh: Long?, type: ChatType, id: Long, itemId: Long): ChatItemInfo? {
|
||||
val r = sendCmd(rh, CC.ApiGetChatItemInfo(type, id, itemId))
|
||||
if (r is API.Result && r.res is CR.ApiChatItemInfo) return r.res.chatItemInfo
|
||||
apiErrorAlert("apiGetChatItemInfo", generalGetString(MR.strings.error_loading_details), r)
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiForwardChatItems(rh: Long?, toChatType: ChatType, toChatId: Long, toScope: GroupChatScope?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, itemIds: List<Long>, ttl: Int?): List<ChatItem>? {
|
||||
val cmd = CC.ApiForwardChatItems(toChatType, toChatId, toScope, fromChatType, fromChatId, fromScope, itemIds, ttl)
|
||||
suspend fun apiForwardChatItems(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemIds: List<Long>, ttl: Int?): List<ChatItem>? {
|
||||
val cmd = CC.ApiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl)
|
||||
return processSendMessageCmd(rh, cmd)?.map { it.chatItem }
|
||||
}
|
||||
|
||||
suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, chatItemIds: List<Long>): CR.ForwardPlan? {
|
||||
val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, fromScope, chatItemIds))
|
||||
suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, chatItemIds: List<Long>): CR.ForwardPlan? {
|
||||
val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, chatItemIds))
|
||||
if (r is API.Result && r.res is CR.ForwardPlan) return r.res
|
||||
apiErrorAlert("apiPlanForwardChatItems", generalGetString(MR.strings.error_forwarding_messages), r)
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiUpdateChatItem(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemId: Long, updatedMessage: UpdatedMessage, live: Boolean = false): AChatItem? {
|
||||
val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, scope, itemId, updatedMessage, live))
|
||||
suspend fun apiUpdateChatItem(rh: Long?, type: ChatType, id: Long, itemId: Long, updatedMessage: UpdatedMessage, live: Boolean = false): AChatItem? {
|
||||
val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, itemId, updatedMessage, live))
|
||||
when {
|
||||
r is API.Result && r.res is CR.ChatItemUpdated -> return r.res.chatItem
|
||||
r is API.Result && r.res is CR.ChatItemNotChanged -> return r.res.chatItem
|
||||
|
@ -1014,8 +1011,8 @@ object ChatController {
|
|||
return null
|
||||
}
|
||||
|
||||
suspend fun apiChatItemReaction(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemId: Long, add: Boolean, reaction: MsgReaction): ChatItem? {
|
||||
val r = sendCmd(rh, CC.ApiChatItemReaction(type, id, scope, itemId, add, reaction))
|
||||
suspend fun apiChatItemReaction(rh: Long?, type: ChatType, id: Long, itemId: Long, add: Boolean, reaction: MsgReaction): ChatItem? {
|
||||
val r = sendCmd(rh, CC.ApiChatItemReaction(type, id, itemId, add, reaction))
|
||||
if (r is API.Result && r.res is CR.ChatItemReaction) return r.res.reaction.chatReaction.chatItem
|
||||
Log.e(TAG, "apiUpdateChatItem bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
|
@ -1029,8 +1026,8 @@ object ChatController {
|
|||
return null
|
||||
}
|
||||
|
||||
suspend fun apiDeleteChatItems(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemIds: List<Long>, mode: CIDeleteMode): List<ChatItemDeletion>? {
|
||||
val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, scope, itemIds, mode))
|
||||
suspend fun apiDeleteChatItems(rh: Long?, type: ChatType, id: Long, itemIds: List<Long>, mode: CIDeleteMode): List<ChatItemDeletion>? {
|
||||
val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, itemIds, mode))
|
||||
if (r is API.Result && r.res is CR.ChatItemsDeleted) return r.res.chatItemDeletions
|
||||
Log.e(TAG, "apiDeleteChatItem bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
|
@ -1662,18 +1659,18 @@ object ChatController {
|
|||
return null
|
||||
}
|
||||
|
||||
suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?): Boolean {
|
||||
val r = sendCmd(rh, CC.ApiChatRead(type, id, scope))
|
||||
suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long): Boolean {
|
||||
val r = sendCmd(rh, CC.ApiChatRead(type, id))
|
||||
if (r.result is CR.CmdOk) return true
|
||||
Log.e(TAG, "apiChatRead bad response: ${r.responseType} ${r.details}")
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun apiChatItemsRead(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemIds: List<Long>): ChatInfo? {
|
||||
val r = sendCmd(rh, CC.ApiChatItemsRead(type, id, scope, itemIds))
|
||||
if (r is API.Result && r.res is CR.ItemsReadForChat) return r.res.chatInfo
|
||||
suspend fun apiChatItemsRead(rh: Long?, type: ChatType, id: Long, itemIds: List<Long>): Boolean {
|
||||
val r = sendCmd(rh, CC.ApiChatItemsRead(type, id, itemIds))
|
||||
if (r.result is CR.CmdOk) return true
|
||||
Log.e(TAG, "apiChatItemsRead bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun apiChatUnread(rh: Long?, type: ChatType, id: Long, unreadChat: Boolean): Boolean {
|
||||
|
@ -1902,18 +1899,9 @@ object ChatController {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun apiAcceptMember(rh: Long?, groupId: Long, groupMemberId: Long, memberRole: GroupMemberRole): Pair<GroupInfo, GroupMember>? {
|
||||
val r = sendCmd(rh, CC.ApiAcceptMember(groupId, groupMemberId, memberRole))
|
||||
if (r is API.Result && r.res is CR.MemberAccepted) return r.res.groupInfo to r.res.member
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiAcceptMember", generalGetString(MR.strings.error_accepting_member), r)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List<Long>, withMessages: Boolean = false): Pair<GroupInfo, List<GroupMember>>? {
|
||||
suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List<Long>, withMessages: Boolean = false): List<GroupMember>? {
|
||||
val r = sendCmd(rh, CC.ApiRemoveMembers(groupId, memberIds, withMessages))
|
||||
if (r is API.Result && r.res is CR.UserDeletedMembers) return r.res.groupInfo to r.res.members
|
||||
if (r is API.Result && r.res is CR.UserDeletedMembers) return r.res.members
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiRemoveMembers", generalGetString(MR.strings.error_removing_member), r)
|
||||
}
|
||||
|
@ -2432,7 +2420,9 @@ object ChatController {
|
|||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.secondaryChatsContext.value?.addChatItem(rhId, cInfo, cItem)
|
||||
if (cItem.isReport) {
|
||||
chatModel.secondaryChatsContext.value?.addChatItem(rhId, cInfo, cItem)
|
||||
}
|
||||
}
|
||||
} else if (cItem.isRcvNew && cInfo.ntfsEnabled(cItem)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
|
@ -2458,10 +2448,12 @@ object ChatController {
|
|||
val cItem = chatItem.chatItem
|
||||
if (!cItem.isDeletedContent && active(r.user)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.upsertChatItem(rhId, cInfo, cItem)
|
||||
chatModel.chatsContext.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.secondaryChatsContext.value?.upsertChatItem(rhId, cInfo, cItem)
|
||||
if (cItem.isReport) {
|
||||
chatModel.secondaryChatsContext.value?.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2473,7 +2465,9 @@ object ChatController {
|
|||
chatModel.chatsContext.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.secondaryChatsContext.value?.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
|
||||
if (r.reaction.chatReaction.chatItem.isReport) {
|
||||
chatModel.secondaryChatsContext.value?.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2516,10 +2510,12 @@ object ChatController {
|
|||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
if (toChatItem == null) {
|
||||
chatModel.secondaryChatsContext.value?.removeChatItem(rhId, cInfo, cItem)
|
||||
} else {
|
||||
chatModel.secondaryChatsContext.value?.upsertChatItem(rhId, cInfo, toChatItem.chatItem)
|
||||
if (cItem.isReport) {
|
||||
if (toChatItem == null) {
|
||||
chatModel.secondaryChatsContext.value?.removeChatItem(rhId, cInfo, cItem)
|
||||
} else {
|
||||
chatModel.secondaryChatsContext.value?.upsertChatItem(rhId, cInfo, toChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2579,13 +2575,6 @@ object ChatController {
|
|||
chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
}
|
||||
is CR.MemberAcceptedByOther ->
|
||||
if (active(r.user)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
chatModel.chatsContext.updateGroup(rhId, r.groupInfo)
|
||||
}
|
||||
}
|
||||
is CR.DeletedMemberUser -> // TODO update user member
|
||||
if (active(r.user)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
|
@ -2603,7 +2592,6 @@ object ChatController {
|
|||
is CR.DeletedMember ->
|
||||
if (active(r.user)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateGroup(rhId, r.groupInfo)
|
||||
chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.deletedMember)
|
||||
if (r.withMessages) {
|
||||
chatModel.chatsContext.removeMemberItems(rhId, r.deletedMember, byMember = r.byMember, r.groupInfo)
|
||||
|
@ -2619,7 +2607,6 @@ object ChatController {
|
|||
is CR.LeftMember ->
|
||||
if (active(r.user)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateGroup(rhId, r.groupInfo)
|
||||
chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
|
@ -2668,16 +2655,6 @@ object ChatController {
|
|||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateGroup(rhId, r.groupInfo)
|
||||
}
|
||||
if (
|
||||
chatModel.chatId.value == r.groupInfo.id
|
||||
&& ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)
|
||||
&& chatModel.secondaryChatsContext.value?.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext
|
||||
) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.secondaryChatsContext.value = null
|
||||
}
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
}
|
||||
is CR.JoinedGroupMember ->
|
||||
if (active(r.user)) {
|
||||
|
@ -3039,11 +3016,11 @@ object ChatController {
|
|||
if (activeUser(rh, user)) {
|
||||
val cInfo = aChatItem.chatInfo
|
||||
val cItem = aChatItem.chatItem
|
||||
withContext(Dispatchers.Main) { chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) }
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem)
|
||||
if (cItem.isReport) {
|
||||
chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3055,7 +3032,7 @@ object ChatController {
|
|||
chatModel.users.addAll(users)
|
||||
return
|
||||
}
|
||||
val cInfo = ChatInfo.Group(r.groupInfo, groupChatScope = null) // TODO [knocking] get scope from items?
|
||||
val cInfo = ChatInfo.Group(r.groupInfo)
|
||||
withContext(Dispatchers.Main) {
|
||||
val chatsCtx = chatModel.chatsContext
|
||||
r.chatItemIDs.forEach { itemId ->
|
||||
|
@ -3110,11 +3087,11 @@ object ChatController {
|
|||
if (!activeUser(rh, user)) {
|
||||
notify()
|
||||
} else {
|
||||
val createdChat = withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem)
|
||||
}
|
||||
val createdChat = withContext(Dispatchers.Main) { chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) }
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem)
|
||||
if (cItem.content.msgContent is MsgContent.MCReport) {
|
||||
chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem)
|
||||
}
|
||||
}
|
||||
if (createdChat) {
|
||||
notify()
|
||||
|
@ -3319,9 +3296,9 @@ sealed class CC {
|
|||
class ApiGetSettings(val settings: AppSettings): CC()
|
||||
class ApiGetChatTags(val userId: Long): CC()
|
||||
class ApiGetChats(val userId: Long): CC()
|
||||
class ApiGetChat(val type: ChatType, val id: Long, val scope: GroupChatScope?, val contentTag: MsgContentTag?, val pagination: ChatPagination, val search: String = ""): CC()
|
||||
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long): CC()
|
||||
class ApiSendMessages(val type: ChatType, val id: Long, val scope: GroupChatScope?, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): CC()
|
||||
class ApiGetChat(val type: ChatType, val id: Long, val contentTag: MsgContentTag?, val pagination: ChatPagination, val search: String = ""): CC()
|
||||
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC()
|
||||
class ApiSendMessages(val type: ChatType, val id: Long, val live: Boolean, val ttl: Int?, val composedMessages: List<ComposedMessage>): CC()
|
||||
class ApiCreateChatTag(val tag: ChatTagData): CC()
|
||||
class ApiSetChatTags(val type: ChatType, val id: Long, val tagIds: List<Long>): CC()
|
||||
class ApiDeleteChatTag(val tagId: Long): CC()
|
||||
|
@ -3329,19 +3306,18 @@ sealed class CC {
|
|||
class ApiReorderChatTags(val tagIds: List<Long>): CC()
|
||||
class ApiCreateChatItems(val noteFolderId: Long, val composedMessages: List<ComposedMessage>): CC()
|
||||
class ApiReportMessage(val groupId: Long, val chatItemId: Long, val reportReason: ReportReason, val reportText: String): CC()
|
||||
class ApiUpdateChatItem(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long, val updatedMessage: UpdatedMessage, val live: Boolean): CC()
|
||||
class ApiDeleteChatItem(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemIds: List<Long>, val mode: CIDeleteMode): CC()
|
||||
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val updatedMessage: UpdatedMessage, val live: Boolean): CC()
|
||||
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemIds: List<Long>, val mode: CIDeleteMode): CC()
|
||||
class ApiDeleteMemberChatItem(val groupId: Long, val itemIds: List<Long>): CC()
|
||||
class ApiArchiveReceivedReports(val groupId: Long): CC()
|
||||
class ApiDeleteReceivedReports(val groupId: Long, val itemIds: List<Long>, val mode: CIDeleteMode): CC()
|
||||
class ApiChatItemReaction(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
|
||||
class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
|
||||
class ApiGetReactionMembers(val userId: Long, val groupId: Long, val itemId: Long, val reaction: MsgReaction): CC()
|
||||
class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val chatItemIds: List<Long>): CC()
|
||||
class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val itemIds: List<Long>, val ttl: Int?): CC()
|
||||
class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val chatItemIds: List<Long>): CC()
|
||||
class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemIds: List<Long>, val ttl: Int?): CC()
|
||||
class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC()
|
||||
class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC()
|
||||
class ApiJoinGroup(val groupId: Long): CC()
|
||||
class ApiAcceptMember(val groupId: Long, val groupMemberId: Long, val memberRole: GroupMemberRole): CC()
|
||||
class ApiMembersRole(val groupId: Long, val memberIds: List<Long>, val memberRole: GroupMemberRole): CC()
|
||||
class ApiBlockMembersForAll(val groupId: Long, val memberIds: List<Long>, val blocked: Boolean): CC()
|
||||
class ApiRemoveMembers(val groupId: Long, val memberIds: List<Long>, val withMessages: Boolean): CC()
|
||||
|
@ -3419,8 +3395,8 @@ sealed class CC {
|
|||
class ApiGetNetworkStatuses(): CC()
|
||||
class ApiAcceptContact(val incognito: Boolean, val contactReqId: Long): CC()
|
||||
class ApiRejectContact(val contactReqId: Long): CC()
|
||||
class ApiChatRead(val type: ChatType, val id: Long, val scope: GroupChatScope?): CC()
|
||||
class ApiChatItemsRead(val type: ChatType, val id: Long, val scope: GroupChatScope?, val itemIds: List<Long>): CC()
|
||||
class ApiChatRead(val type: ChatType, val id: Long): CC()
|
||||
class ApiChatItemsRead(val type: ChatType, val id: Long, val itemIds: List<Long>): CC()
|
||||
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
|
||||
class ReceiveFile(val fileId: Long, val userApprovedRelays: Boolean, val encrypt: Boolean, val inline: Boolean?): CC()
|
||||
class CancelFile(val fileId: Long): CC()
|
||||
|
@ -3492,16 +3468,16 @@ sealed class CC {
|
|||
} else {
|
||||
" content=${contentTag.name.lowercase()}"
|
||||
}
|
||||
"/_get chat ${chatRef(type, id, scope)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
||||
"/_get chat ${chatRef(type, id)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
|
||||
}
|
||||
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id, scope)} $itemId"
|
||||
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
|
||||
is ApiSendMessages -> {
|
||||
val msgs = json.encodeToString(composedMessages)
|
||||
val ttlStr = if (ttl != null) "$ttl" else "default"
|
||||
"/_send ${chatRef(type, id, scope)} live=${onOff(live)} ttl=${ttlStr} json $msgs"
|
||||
"/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json $msgs"
|
||||
}
|
||||
is ApiCreateChatTag -> "/_create tag ${json.encodeToString(tag)}"
|
||||
is ApiSetChatTags -> "/_tags ${chatRef(type, id, scope = null)} ${tagIds.joinToString(",")}"
|
||||
is ApiSetChatTags -> "/_tags ${chatRef(type, id)} ${tagIds.joinToString(",")}"
|
||||
is ApiDeleteChatTag -> "/_delete tag $tagId"
|
||||
is ApiUpdateChatTag -> "/_update tag $tagId ${json.encodeToString(tagData)}"
|
||||
is ApiReorderChatTags -> "/_reorder tags ${tagIds.joinToString(",")}"
|
||||
|
@ -3510,24 +3486,23 @@ sealed class CC {
|
|||
"/_create *$noteFolderId json $msgs"
|
||||
}
|
||||
is ApiReportMessage -> "/_report #$groupId $chatItemId reason=${json.encodeToString(reportReason).trim('"')} $reportText"
|
||||
is ApiUpdateChatItem -> "/_update item ${chatRef(type, id, scope)} $itemId live=${onOff(live)} ${updatedMessage.cmdString}"
|
||||
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id, scope)} ${itemIds.joinToString(",")} ${mode.deleteMode}"
|
||||
is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${updatedMessage.cmdString}"
|
||||
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} ${itemIds.joinToString(",")} ${mode.deleteMode}"
|
||||
is ApiDeleteMemberChatItem -> "/_delete member item #$groupId ${itemIds.joinToString(",")}"
|
||||
is ApiArchiveReceivedReports -> "/_archive reports #$groupId"
|
||||
is ApiDeleteReceivedReports -> "/_delete reports #$groupId ${itemIds.joinToString(",")} ${mode.deleteMode}"
|
||||
is ApiChatItemReaction -> "/_reaction ${chatRef(type, id, scope)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}"
|
||||
is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}"
|
||||
is ApiGetReactionMembers -> "/_reaction members $userId #$groupId $itemId ${json.encodeToString(reaction)}"
|
||||
is ApiForwardChatItems -> {
|
||||
val ttlStr = if (ttl != null) "$ttl" else "default"
|
||||
"/_forward ${chatRef(toChatType, toChatId, toScope)} ${chatRef(fromChatType, fromChatId, fromScope)} ${itemIds.joinToString(",")} ttl=${ttlStr}"
|
||||
"/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} ${itemIds.joinToString(",")} ttl=${ttlStr}"
|
||||
}
|
||||
is ApiPlanForwardChatItems -> {
|
||||
"/_forward plan ${chatRef(fromChatType, fromChatId, fromScope)} ${chatItemIds.joinToString(",")}"
|
||||
"/_forward plan ${chatRef(fromChatType, fromChatId)} ${chatItemIds.joinToString(",")}"
|
||||
}
|
||||
is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}"
|
||||
is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}"
|
||||
is ApiJoinGroup -> "/_join #$groupId"
|
||||
is ApiAcceptMember -> "/_accept member #$groupId $groupMemberId ${memberRole.memberRole}"
|
||||
is ApiMembersRole -> "/_member role #$groupId ${memberIds.joinToString(",")} ${memberRole.memberRole}"
|
||||
is ApiBlockMembersForAll -> "/_block #$groupId ${memberIds.joinToString(",")} blocked=${onOff(blocked)}"
|
||||
is ApiRemoveMembers -> "/_remove #$groupId ${memberIds.joinToString(",")} messages=${onOff(withMessages)}"
|
||||
|
@ -3551,13 +3526,13 @@ sealed class CC {
|
|||
is ApiAcceptConditions -> "/_accept_conditions ${conditionsId} ${operatorIds.joinToString(",")}"
|
||||
is APISetChatItemTTL -> "/_ttl $userId ${chatItemTTLStr(seconds)}"
|
||||
is APIGetChatItemTTL -> "/_ttl $userId"
|
||||
is APISetChatTTL -> "/_ttl $userId ${chatRef(chatType, id, scope = null)} ${chatItemTTLStr(seconds)}"
|
||||
is APISetChatTTL -> "/_ttl $userId ${chatRef(chatType, id)} ${chatItemTTLStr(seconds)}"
|
||||
is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}"
|
||||
is APIGetNetworkConfig -> "/network"
|
||||
is APISetNetworkInfo -> "/_network info ${json.encodeToString(networkInfo)}"
|
||||
is ReconnectServer -> "/reconnect $userId $server"
|
||||
is ReconnectAllServers -> "/reconnect"
|
||||
is APISetChatSettings -> "/_settings ${chatRef(type, id, scope = null)} ${json.encodeToString(chatSettings)}"
|
||||
is APISetChatSettings -> "/_settings ${chatRef(type, id)} ${json.encodeToString(chatSettings)}"
|
||||
is ApiSetMemberSettings -> "/_member settings #$groupId $groupMemberId ${json.encodeToString(memberSettings)}"
|
||||
is APIContactInfo -> "/_info @$contactId"
|
||||
is APIGroupMemberInfo -> "/_info #$groupId $groupMemberId"
|
||||
|
@ -3579,8 +3554,8 @@ sealed class CC {
|
|||
is APIConnectPlan -> "/_connect plan $userId $connLink"
|
||||
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} ${connLink.connFullLink} ${connLink.connShortLink ?: ""}"
|
||||
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
|
||||
is ApiDeleteChat -> "/_delete ${chatRef(type, id, scope = null)} ${chatDeleteMode.cmdString}"
|
||||
is ApiClearChat -> "/_clear chat ${chatRef(type, id, scope = null)}"
|
||||
is ApiDeleteChat -> "/_delete ${chatRef(type, id)} ${chatDeleteMode.cmdString}"
|
||||
is ApiClearChat -> "/_clear chat ${chatRef(type, id)}"
|
||||
is ApiListContacts -> "/_contacts $userId"
|
||||
is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}"
|
||||
is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}"
|
||||
|
@ -3605,9 +3580,9 @@ sealed class CC {
|
|||
is ApiEndCall -> "/_call end @${contact.apiId}"
|
||||
is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}"
|
||||
is ApiGetNetworkStatuses -> "/_network_statuses"
|
||||
is ApiChatRead -> "/_read chat ${chatRef(type, id, scope)}"
|
||||
is ApiChatItemsRead -> "/_read chat items ${chatRef(type, id, scope)} ${itemIds.joinToString(",")}"
|
||||
is ApiChatUnread -> "/_unread chat ${chatRef(type, id, scope = null)} ${onOff(unreadChat)}"
|
||||
is ApiChatRead -> "/_read chat ${chatRef(type, id)}"
|
||||
is ApiChatItemsRead -> "/_read chat items ${chatRef(type, id)} ${itemIds.joinToString(",")}"
|
||||
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
|
||||
is ReceiveFile ->
|
||||
"/freceive $fileId" +
|
||||
" approved_relays=${onOff(userApprovedRelays)}" +
|
||||
|
@ -3691,7 +3666,6 @@ sealed class CC {
|
|||
is ApiNewGroup -> "apiNewGroup"
|
||||
is ApiAddMember -> "apiAddMember"
|
||||
is ApiJoinGroup -> "apiJoinGroup"
|
||||
is ApiAcceptMember -> "apiAcceptMember"
|
||||
is ApiMembersRole -> "apiMembersRole"
|
||||
is ApiBlockMembersForAll -> "apiBlockMembersForAll"
|
||||
is ApiRemoveMembers -> "apiRemoveMembers"
|
||||
|
@ -3828,13 +3802,7 @@ sealed class CC {
|
|||
private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd)
|
||||
|
||||
companion object {
|
||||
fun chatRef(chatType: ChatType, id: Long, scope: GroupChatScope?) = when (scope) {
|
||||
null -> "${chatType.type}${id}"
|
||||
is GroupChatScope.MemberSupport -> when (scope.groupMemberId_) {
|
||||
null -> "${chatType.type}${id}(_support)"
|
||||
else -> "${chatType.type}${id}(_support:${scope.groupMemberId_})"
|
||||
}
|
||||
}
|
||||
fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5796,7 +5764,6 @@ sealed class CR {
|
|||
@Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR()
|
||||
@Serializable @SerialName("contactDeleted") class ContactDeleted(val user: UserRef, val contact: Contact): CR()
|
||||
@Serializable @SerialName("contactDeletedByContact") class ContactDeletedByContact(val user: UserRef, val contact: Contact): CR()
|
||||
@Serializable @SerialName("itemsReadForChat") class ItemsReadForChat(val user: UserRef, val chatInfo: ChatInfo): CR()
|
||||
@Serializable @SerialName("chatCleared") class ChatCleared(val user: UserRef, val chatInfo: ChatInfo): CR()
|
||||
@Serializable @SerialName("userProfileNoChange") class UserProfileNoChange(val user: User): CR()
|
||||
@Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val user: User, val fromProfile: Profile, val toProfile: Profile, val updateSummary: UserProfileUpdateSummary): CR()
|
||||
|
@ -5845,8 +5812,6 @@ sealed class CR {
|
|||
@Serializable @SerialName("receivedGroupInvitation") class ReceivedGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val memberRole: GroupMemberRole): CR()
|
||||
@Serializable @SerialName("groupDeletedUser") class GroupDeletedUser(val user: UserRef, val groupInfo: GroupInfo): CR()
|
||||
@Serializable @SerialName("joinedGroupMemberConnecting") class JoinedGroupMemberConnecting(val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("memberAccepted") class MemberAccepted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("memberAcceptedByOther") class MemberAcceptedByOther(val user: UserRef, val groupInfo: GroupInfo, val acceptingMember: GroupMember, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("memberRole") class MemberRole(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR()
|
||||
@Serializable @SerialName("membersRoleUser") class MembersRoleUser(val user: UserRef, val groupInfo: GroupInfo, val members: List<GroupMember>, val toRole: GroupMemberRole): CR()
|
||||
@Serializable @SerialName("memberBlockedForAll") class MemberBlockedForAll(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val blocked: Boolean): CR()
|
||||
|
@ -5977,7 +5942,6 @@ sealed class CR {
|
|||
is ContactAlreadyExists -> "contactAlreadyExists"
|
||||
is ContactDeleted -> "contactDeleted"
|
||||
is ContactDeletedByContact -> "contactDeletedByContact"
|
||||
is ItemsReadForChat -> "itemsReadForChat"
|
||||
is ChatCleared -> "chatCleared"
|
||||
is UserProfileNoChange -> "userProfileNoChange"
|
||||
is UserProfileUpdated -> "userProfileUpdated"
|
||||
|
@ -6023,8 +5987,6 @@ sealed class CR {
|
|||
is ReceivedGroupInvitation -> "receivedGroupInvitation"
|
||||
is GroupDeletedUser -> "groupDeletedUser"
|
||||
is JoinedGroupMemberConnecting -> "joinedGroupMemberConnecting"
|
||||
is MemberAccepted -> "memberAccepted"
|
||||
is MemberAcceptedByOther -> "memberAcceptedByOther"
|
||||
is MemberRole -> "memberRole"
|
||||
is MembersRoleUser -> "membersRoleUser"
|
||||
is MemberBlockedForAll -> "memberBlockedForAll"
|
||||
|
@ -6148,7 +6110,6 @@ sealed class CR {
|
|||
is ContactAlreadyExists -> withUser(user, json.encodeToString(contact))
|
||||
is ContactDeleted -> withUser(user, json.encodeToString(contact))
|
||||
is ContactDeletedByContact -> withUser(user, json.encodeToString(contact))
|
||||
is ItemsReadForChat -> withUser(user, json.encodeToString(chatInfo))
|
||||
is ChatCleared -> withUser(user, json.encodeToString(chatInfo))
|
||||
is UserProfileNoChange -> withUser(user, noDetails())
|
||||
is UserProfileUpdated -> withUser(user, json.encodeToString(toProfile))
|
||||
|
@ -6194,8 +6155,6 @@ sealed class CR {
|
|||
is ReceivedGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmemberRole: $memberRole")
|
||||
is GroupDeletedUser -> withUser(user, json.encodeToString(groupInfo))
|
||||
is JoinedGroupMemberConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember\nmember: $member")
|
||||
is MemberAccepted -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
|
||||
is MemberAcceptedByOther -> withUser(user, "groupInfo: $groupInfo\nacceptingMember: $acceptingMember\nmember: $member")
|
||||
is MemberRole -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nfromRole: $fromRole\ntoRole: $toRole")
|
||||
is MembersRoleUser -> withUser(user, "groupInfo: $groupInfo\nmembers: $members\ntoRole: $toRole")
|
||||
is MemberBlockedForAll -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nblocked: $blocked")
|
||||
|
|
|
@ -12,11 +12,10 @@ import java.net.URI
|
|||
expect fun PlatformTextField(
|
||||
composeState: MutableState<ComposeState>,
|
||||
sendMsgEnabled: Boolean,
|
||||
disabledText: String?,
|
||||
sendMsgButtonDisabled: Boolean,
|
||||
textStyle: MutableState<TextStyle>,
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
userIsPending: Boolean,
|
||||
placeholder: String,
|
||||
showVoiceButton: Boolean,
|
||||
onMessageChange: (ComposeMessage) -> Unit,
|
||||
|
|
|
@ -99,13 +99,11 @@ fun TerminalLayout(
|
|||
isDirectChat = false,
|
||||
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
||||
sendMsgEnabled = true,
|
||||
userCantSendReason = null,
|
||||
sendButtonEnabled = true,
|
||||
nextSendGrpInv = false,
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = false,
|
||||
userIsObserver = false,
|
||||
userIsPending = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
placeholder = "",
|
||||
sendMessage = { sendCommand() },
|
||||
|
|
|
@ -40,8 +40,9 @@ import chat.simplex.common.ui.theme.*
|
|||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.usersettings.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.group.ChatTTLOption
|
||||
import chat.simplex.common.views.chat.group.ChatTTLSection
|
||||
import chat.simplex.common.views.chatlist.updateChatSettings
|
||||
import chat.simplex.common.views.database.*
|
||||
import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
|
@ -616,10 +617,7 @@ fun ChatInfoLayout(
|
|||
}
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
|
||||
SectionView {
|
||||
ChatTTLOption(chatItemTTL, setChatItemTTL, deletingItems)
|
||||
SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer))
|
||||
}
|
||||
ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems)
|
||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
|
||||
|
||||
val conn = contact.activeConn
|
||||
|
@ -1386,7 +1384,7 @@ private fun setChatTTL(
|
|||
private suspend fun afterSetChatTTL(chatsCtx: ChatModel.ChatsContext, rhId: Long?, chatInfo: ChatInfo, progressIndicator: MutableState<Boolean>) {
|
||||
try {
|
||||
val pagination = ChatPagination.Initial(ChatPagination.INITIAL_COUNT)
|
||||
val (chat, navInfo) = controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, scope = null, contentTag = null, pagination) ?: return
|
||||
val (chat, navInfo) = controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, null, pagination) ?: return
|
||||
if (chat.chatItems.isEmpty()) {
|
||||
// replacing old chat with the same old chat but without items. Less intrusive way of clearing a preview
|
||||
withContext(Dispatchers.Main) {
|
||||
|
|
|
@ -17,7 +17,7 @@ suspend fun apiLoadSingleMessage(
|
|||
apiId: Long,
|
||||
itemId: Long
|
||||
): ChatItem? = coroutineScope {
|
||||
val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), chatsCtx.contentTag, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null
|
||||
val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.contentTag, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null
|
||||
chat.chatItems.firstOrNull()
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ suspend fun apiLoadMessages(
|
|||
openAroundItemId: Long? = null,
|
||||
visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 }
|
||||
) = coroutineScope {
|
||||
val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), chatsCtx.contentTag, pagination, search) ?: return@coroutineScope
|
||||
val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.contentTag, pagination, search) ?: return@coroutineScope
|
||||
// For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes
|
||||
/** When [openAroundItemId] is provided, chatId can be different too */
|
||||
if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last && openAroundItemId == null)
|
||||
|
@ -54,7 +54,7 @@ suspend fun processLoadedChat(
|
|||
when (pagination) {
|
||||
is ChatPagination.Initial -> {
|
||||
val newSplits = if (chat.chatItems.isNotEmpty() && navInfo.afterTotal > 0) listOf(chat.chatItems.last().id) else emptyList()
|
||||
if (chatsCtx.secondaryContextFilter == null) {
|
||||
if (chatsCtx.contentTag == null) {
|
||||
// update main chats, not content tagged
|
||||
withContext(Dispatchers.Main) {
|
||||
val oldChat = chatModel.chatsContext.getChat(chat.id)
|
||||
|
@ -68,6 +68,7 @@ suspend fun processLoadedChat(
|
|||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
chatsCtx.chatItemStatuses.clear()
|
||||
chatsCtx.chatItems.replaceAll(chat.chatItems)
|
||||
chatModel.chatId.value = chat.id
|
||||
splits.value = newSplits
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,117 +0,0 @@
|
|||
package chat.simplex.common.views.chat
|
||||
|
||||
import SectionItemView
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.views.chat.group.removeMemberDialog
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun ComposeContextPendingMemberActionsView(
|
||||
rhId: Long?,
|
||||
groupInfo: GroupInfo,
|
||||
member: GroupMember
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.height(60.dp)
|
||||
.background(MaterialTheme.colors.surface)
|
||||
) {
|
||||
Divider()
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.weight(1F)
|
||||
.clickable {
|
||||
removeMemberDialog(rhId, groupInfo, member, chatModel, close = { ModalManager.end.closeModal() })
|
||||
},
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(stringResource(MR.strings.remove_pending_member_button), color = Color.Red)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.weight(1F)
|
||||
.clickable {
|
||||
acceptMemberDialog(rhId, groupInfo, member, close = { ModalManager.end.closeModal() })
|
||||
},
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(stringResource(MR.strings.accept_pending_member_button), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun acceptMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, close: (() -> Unit)? = null) {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.accept_pending_member_alert_title),
|
||||
text = generalGetString(MR.strings.accept_pending_member_alert_question),
|
||||
buttons = {
|
||||
Column {
|
||||
// Accept as member
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
acceptMember(rhId, groupInfo, member, GroupMemberRole.Member, close)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.accept_pending_member_alert_confirmation_as_member), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
// Accept as observer
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
acceptMember(rhId, groupInfo, member, GroupMemberRole.Observer, close)
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.accept_pending_member_alert_confirmation_as_observer), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
// Cancel
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun acceptMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, role: GroupMemberRole, close: (() -> Unit)?) {
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiAcceptMember(rhId, groupInfo.groupId, member.groupMemberId, role)
|
||||
if (r != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, r.second)
|
||||
chatModel.chatsContext.updateGroup(rhId, r.first)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, r.second)
|
||||
}
|
||||
}
|
||||
close?.invoke()
|
||||
}
|
||||
}
|
|
@ -333,9 +333,7 @@ suspend fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text:
|
|||
|
||||
@Composable
|
||||
fun ComposeView(
|
||||
rhId: Long?,
|
||||
chatModel: ChatModel,
|
||||
chatsCtx: ChatModel.ChatsContext,
|
||||
chat: Chat,
|
||||
composeState: MutableState<ComposeState>,
|
||||
attachmentOption: MutableState<AttachmentOption?>,
|
||||
|
@ -468,7 +466,6 @@ fun ComposeView(
|
|||
rh = chat.remoteHostId,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
scope = cInfo.groupChatScope(),
|
||||
live = live,
|
||||
ttl = ttl,
|
||||
composedMessages = listOf(ComposedMessage(file, quoted, mc, mentions))
|
||||
|
@ -476,7 +473,7 @@ fun ComposeView(
|
|||
if (!chatItems.isNullOrEmpty()) {
|
||||
chatItems.forEach { aChatItem ->
|
||||
withContext(Dispatchers.Main) {
|
||||
chatsCtx.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
|
||||
chatModel.chatsContext.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
return chatItems.first().chatItem
|
||||
|
@ -501,17 +498,15 @@ fun ComposeView(
|
|||
rh = rhId,
|
||||
toChatType = chat.chatInfo.chatType,
|
||||
toChatId = chat.chatInfo.apiId,
|
||||
toScope = chat.chatInfo.groupChatScope(),
|
||||
fromChatType = fromChatInfo.chatType,
|
||||
fromChatId = fromChatInfo.apiId,
|
||||
fromScope = fromChatInfo.groupChatScope(),
|
||||
itemIds = forwardedItem.map { it.id },
|
||||
ttl = ttl
|
||||
)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
chatItems?.forEach { chatItem ->
|
||||
chatsCtx.addChatItem(rhId, chat.chatInfo, chatItem)
|
||||
chatModel.chatsContext.addChatItem(rhId, chat.chatInfo, chatItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -568,21 +563,16 @@ fun ComposeView(
|
|||
}
|
||||
}
|
||||
|
||||
fun showReportsInSupportChatAlert() {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.report_sent_alert_title),
|
||||
text = generalGetString(MR.strings.report_sent_alert_msg_view_in_support_chat),
|
||||
confirmText = generalGetString(MR.strings.ok),
|
||||
dismissText = generalGetString(MR.strings.dont_show_again),
|
||||
onDismiss = {
|
||||
chatModel.controller.appPrefs.showReportsInSupportChatAlert.set(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun sendReport(reportReason: ReportReason, chatItemId: Long): List<ChatItem>? {
|
||||
val cItems = chatModel.controller.apiReportMessage(chat.remoteHostId, chat.chatInfo.apiId, chatItemId, reportReason, msgText)
|
||||
if (chatModel.controller.appPrefs.showReportsInSupportChatAlert.get()) showReportsInSupportChatAlert()
|
||||
if (cItems != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
cItems.forEach { chatItem ->
|
||||
chatModel.chatsContext.addChatItem(chat.remoteHostId, chat.chatInfo, chatItem.chatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cItems?.map { it.chatItem }
|
||||
}
|
||||
|
||||
|
@ -591,7 +581,7 @@ fun ComposeView(
|
|||
val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc)
|
||||
if (contact != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatsCtx.updateContact(chat.remoteHostId, contact)
|
||||
chatModel.chatsContext.updateContact(chat.remoteHostId, contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -604,14 +594,13 @@ fun ComposeView(
|
|||
rh = chat.remoteHostId,
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
scope = cInfo.groupChatScope(),
|
||||
itemId = ei.meta.itemId,
|
||||
updatedMessage = UpdatedMessage(updateMsgContent(oldMsgContent), cs.memberMentions),
|
||||
live = live
|
||||
)
|
||||
if (updatedItem != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatsCtx.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem)
|
||||
chatModel.chatsContext.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem)
|
||||
}
|
||||
}
|
||||
return updatedItem?.chatItem
|
||||
|
@ -902,7 +891,7 @@ fun ComposeView(
|
|||
|
||||
fun editPrevMessage() {
|
||||
if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return
|
||||
val lastEditable = chatsCtx.chatItems.value.findLast { it.meta.editable }
|
||||
val lastEditable = chatModel.chatsContext.chatItems.value.findLast { it.meta.editable }
|
||||
if (lastEditable != null) {
|
||||
composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews)
|
||||
}
|
||||
|
@ -1010,28 +999,11 @@ fun ComposeView(
|
|||
chatModel.sharedContent.value = null
|
||||
}
|
||||
|
||||
val userCanSend = rememberUpdatedState(chat.chatInfo.userCanSend)
|
||||
val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled)
|
||||
val userIsObserver = rememberUpdatedState(chat.userIsObserver)
|
||||
val userIsPending = rememberUpdatedState(chat.userIsPending)
|
||||
val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason)
|
||||
val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv)
|
||||
|
||||
Column {
|
||||
if (
|
||||
chat.chatInfo is ChatInfo.Group
|
||||
&& chatsCtx.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext
|
||||
&& chatsCtx.secondaryContextFilter.groupScopeInfo is GroupChatScopeInfo.MemberSupport
|
||||
&& chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_ != null
|
||||
&& chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_.memberPending
|
||||
&& composeState.value.contextItem == ComposeContextItem.NoContextItem
|
||||
&& composeState.value.preview == ComposePreview.NoPreview
|
||||
) {
|
||||
ComposeContextPendingMemberActionsView(
|
||||
rhId = rhId,
|
||||
groupInfo = chat.chatInfo.groupInfo,
|
||||
member = chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_
|
||||
)
|
||||
}
|
||||
if (nextSendGrpInv.value) {
|
||||
ComposeContextInvitingContactMemberView()
|
||||
}
|
||||
|
@ -1083,7 +1055,6 @@ fun ComposeView(
|
|||
val attachmentEnabled =
|
||||
!composeState.value.attachmentDisabled
|
||||
&& sendMsgEnabled.value
|
||||
&& userCanSend.value
|
||||
&& !isGroupAndProhibitedFiles
|
||||
&& !nextSendGrpInv.value
|
||||
IconButton(
|
||||
|
@ -1129,8 +1100,8 @@ fun ComposeView(
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(rememberUpdatedState(chat.chatInfo.userCanSend).value) {
|
||||
if (!chat.chatInfo.userCanSend) {
|
||||
LaunchedEffect(rememberUpdatedState(chat.chatInfo.sendMsgEnabled).value) {
|
||||
if (!chat.chatInfo.sendMsgEnabled) {
|
||||
clearCurrentDraft()
|
||||
clearState()
|
||||
}
|
||||
|
@ -1186,14 +1157,12 @@ fun ComposeView(
|
|||
chat.chatInfo is ChatInfo.Direct,
|
||||
liveMessageAlertShown = chatModel.controller.appPrefs.liveMessageAlertShown,
|
||||
sendMsgEnabled = sendMsgEnabled.value,
|
||||
userCantSendReason = userCantSendReason.value,
|
||||
sendButtonEnabled = sendMsgEnabled.value && !(simplexLinkProhibited || fileProhibited || voiceProhibited),
|
||||
nextSendGrpInv = nextSendGrpInv.value,
|
||||
needToAllowVoiceToContact,
|
||||
allowedVoiceByPrefs,
|
||||
allowVoiceToContact = ::allowVoiceToContact,
|
||||
userIsObserver = if (chatsCtx.secondaryContextFilter == null) userIsObserver.value else false,
|
||||
userIsPending = if (chatsCtx.secondaryContextFilter == null) userIsPending.value else false,
|
||||
userCanSend = userCanSend.value,
|
||||
sendButtonColor = sendButtonColor,
|
||||
timedMessageAllowed = timedMessageAllowed,
|
||||
customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime,
|
||||
|
|
|
@ -77,7 +77,7 @@ fun SelectedItemsButtonsToolbar(
|
|||
val forwardCountProhibited = remember { mutableStateOf(false) }
|
||||
Box {
|
||||
// It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty
|
||||
ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() })
|
||||
ComposeView(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() })
|
||||
Row(
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
|
@ -101,21 +101,21 @@ fun SelectedItemsButtonsToolbar(
|
|||
)
|
||||
}
|
||||
|
||||
IconButton({ moderateItems() }, Modifier.alpha(if (canModerate.value) 1f else 0f), enabled = moderateEnabled.value && !deleteCountProhibited.value && chatsCtx.secondaryContextFilter == null) {
|
||||
IconButton({ moderateItems() }, Modifier.alpha(if (canModerate.value) 1f else 0f), enabled = moderateEnabled.value && !deleteCountProhibited.value) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_flag),
|
||||
null,
|
||||
Modifier.size(22.dp),
|
||||
tint = if (!moderateEnabled.value || deleteCountProhibited.value || chatsCtx.secondaryContextFilter != null) MaterialTheme.colors.secondary else MaterialTheme.colors.error
|
||||
tint = if (!moderateEnabled.value || deleteCountProhibited.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
|
||||
IconButton({ forwardItems() }, enabled = forwardEnabled.value && !forwardCountProhibited.value && chatsCtx.secondaryContextFilter == null) {
|
||||
IconButton({ forwardItems() }, enabled = forwardEnabled.value && !forwardCountProhibited.value) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_forward),
|
||||
null,
|
||||
Modifier.size(22.dp),
|
||||
tint = if (!forwardEnabled.value || forwardCountProhibited.value || chatsCtx.secondaryContextFilter != null) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
tint = if (!forwardEnabled.value || forwardCountProhibited.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,13 +39,11 @@ fun SendMsgView(
|
|||
isDirectChat: Boolean,
|
||||
liveMessageAlertShown: SharedPreference<Boolean>,
|
||||
sendMsgEnabled: Boolean,
|
||||
userCantSendReason: Pair<String, String?>?,
|
||||
sendButtonEnabled: Boolean,
|
||||
nextSendGrpInv: Boolean,
|
||||
needToAllowVoiceToContact: Boolean,
|
||||
allowedVoiceByPrefs: Boolean,
|
||||
userIsObserver: Boolean,
|
||||
userIsPending: Boolean,
|
||||
userCanSend: Boolean,
|
||||
sendButtonColor: Color = MaterialTheme.colors.primary,
|
||||
allowVoiceToContact: () -> Unit,
|
||||
timedMessageAllowed: Boolean = false,
|
||||
|
@ -81,15 +79,14 @@ fun SendMsgView(
|
|||
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
|
||||
cs.endLiveDisabled ||
|
||||
!sendButtonEnabled
|
||||
val clicksOnTextFieldDisabled = !sendMsgEnabled || cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress
|
||||
val clicksOnTextFieldDisabled = !sendMsgEnabled || cs.preview is ComposePreview.VoicePreview || cs.inProgress
|
||||
PlatformTextField(
|
||||
composeState,
|
||||
sendMsgEnabled,
|
||||
disabledText = userCantSendReason?.first,
|
||||
sendMsgButtonDisabled,
|
||||
textStyle,
|
||||
showDeleteTextButton,
|
||||
userIsObserver,
|
||||
userIsPending,
|
||||
if (clicksOnTextFieldDisabled) "" else placeholder,
|
||||
showVoiceButton,
|
||||
onMessageChange,
|
||||
|
@ -102,16 +99,23 @@ fun SendMsgView(
|
|||
}
|
||||
}
|
||||
if (clicksOnTextFieldDisabled) {
|
||||
Box(
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.clickable(enabled = !userCanSend, indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.observer_cant_send_message_title),
|
||||
text = generalGetString(MR.strings.observer_cant_send_message_desc)
|
||||
)
|
||||
})
|
||||
)
|
||||
if (userCantSendReason != null) {
|
||||
Box(
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.clickable(indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.cant_send_message_alert_title),
|
||||
text = userCantSendReason.second
|
||||
)
|
||||
})
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showDeleteTextButton.value) {
|
||||
DeleteTextButton(composeState)
|
||||
|
@ -135,11 +139,11 @@ fun SendMsgView(
|
|||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val stopRecOnNextClick = remember { mutableStateOf(false) }
|
||||
when {
|
||||
needToAllowVoiceToContact || !allowedVoiceByPrefs || !userCanSend -> {
|
||||
DisallowedVoiceButton(userCanSend) {
|
||||
needToAllowVoiceToContact || !allowedVoiceByPrefs -> {
|
||||
DisallowedVoiceButton {
|
||||
if (needToAllowVoiceToContact) {
|
||||
showNeedToAllowVoiceAlert(allowVoiceToContact)
|
||||
} else if (!allowedVoiceByPrefs) {
|
||||
} else {
|
||||
showDisabledVoiceAlert(isDirectChat)
|
||||
}
|
||||
}
|
||||
|
@ -155,7 +159,7 @@ fun SendMsgView(
|
|||
&& cs.contextItem is ComposeContextItem.NoContextItem
|
||||
) {
|
||||
Spacer(Modifier.width(12.dp))
|
||||
StartLiveMessageButton(userCanSend) {
|
||||
StartLiveMessageButton {
|
||||
if (composeState.value.preview is ComposePreview.NoPreview) {
|
||||
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
|
||||
}
|
||||
|
@ -343,8 +347,8 @@ private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNex
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
IconButton(onClick, Modifier.size(36.dp), enabled = enabled) {
|
||||
private fun DisallowedVoiceButton(onClick: () -> Unit) {
|
||||
IconButton(onClick, Modifier.size(36.dp)) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_keyboard_voice),
|
||||
stringResource(MR.strings.icon_descr_record_voice_message),
|
||||
|
@ -460,14 +464,13 @@ private fun SendMsgButton(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
private fun StartLiveMessageButton(onClick: () -> Unit) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val ripple = remember { ripple(bounded = false, radius = 24.dp) }
|
||||
Box(
|
||||
modifier = Modifier.requiredSize(36.dp)
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
role = Role.Button,
|
||||
interactionSource = interactionSource,
|
||||
indication = ripple
|
||||
|
@ -477,7 +480,7 @@ private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
|
|||
Icon(
|
||||
BoltFilled,
|
||||
stringResource(MR.strings.icon_descr_send_message),
|
||||
tint = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.padding(4.dp)
|
||||
|
@ -576,13 +579,11 @@ fun PreviewSendMsgView() {
|
|||
isDirectChat = true,
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
sendMsgEnabled = true,
|
||||
userCantSendReason = null,
|
||||
sendButtonEnabled = true,
|
||||
nextSendGrpInv = false,
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = true,
|
||||
userIsObserver = false,
|
||||
userIsPending = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
timedMessageAllowed = false,
|
||||
placeholder = "",
|
||||
|
@ -613,13 +614,11 @@ fun PreviewSendMsgViewEditing() {
|
|||
isDirectChat = true,
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
sendMsgEnabled = true,
|
||||
userCantSendReason = null,
|
||||
sendButtonEnabled = true,
|
||||
nextSendGrpInv = false,
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = true,
|
||||
userIsObserver = false,
|
||||
userIsPending = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
timedMessageAllowed = false,
|
||||
placeholder = "",
|
||||
|
@ -650,13 +649,11 @@ fun PreviewSendMsgViewInProgress() {
|
|||
isDirectChat = true,
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
sendMsgEnabled = true,
|
||||
userCantSendReason = null,
|
||||
sendButtonEnabled = true,
|
||||
nextSendGrpInv = false,
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = true,
|
||||
userIsObserver = false,
|
||||
userIsPending = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
timedMessageAllowed = false,
|
||||
placeholder = "",
|
||||
|
|
|
@ -55,16 +55,6 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea
|
|||
GroupPreferencesView(chatModel, rhId, groupInfo.id, close)
|
||||
}
|
||||
},
|
||||
openMemberAdmission = {
|
||||
ModalManager.end.showCustomModal { close ->
|
||||
MemberAdmissionView(
|
||||
chat.simplex.common.platform.chatModel,
|
||||
rhId,
|
||||
groupInfo.id,
|
||||
close
|
||||
)
|
||||
}
|
||||
},
|
||||
inviteMembers = {
|
||||
allowModifyMembers = false
|
||||
withLongRunningApi(slow = 120_000) {
|
||||
|
@ -120,7 +110,6 @@ fun AddGroupMembersLayout(
|
|||
allowModifyMembers: Boolean,
|
||||
searchText: MutableState<TextFieldValue>,
|
||||
openPreferences: () -> Unit,
|
||||
openMemberAdmission: () -> Unit,
|
||||
inviteMembers: () -> Unit,
|
||||
clearSelection: () -> Unit,
|
||||
addContact: (Long) -> Unit,
|
||||
|
@ -155,7 +144,7 @@ fun AddGroupMembersLayout(
|
|||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
ChatInfoToolbarTitle(
|
||||
ChatInfo.Group(groupInfo, groupChatScope = null),
|
||||
ChatInfo.Group(groupInfo),
|
||||
imageSize = 60.dp,
|
||||
iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight
|
||||
)
|
||||
|
@ -176,9 +165,6 @@ fun AddGroupMembersLayout(
|
|||
} else {
|
||||
SectionView {
|
||||
if (creatingGroup) {
|
||||
SectionItemView(openMemberAdmission) {
|
||||
Text(stringResource(MR.strings.set_member_admission))
|
||||
}
|
||||
SectionItemView(openPreferences) {
|
||||
Text(stringResource(MR.strings.set_group_preferences))
|
||||
}
|
||||
|
@ -390,7 +376,6 @@ fun PreviewAddGroupMembersLayout() {
|
|||
allowModifyMembers = true,
|
||||
searchText = remember { mutableStateOf(TextFieldValue("")) },
|
||||
openPreferences = {},
|
||||
openMemberAdmission = {},
|
||||
inviteMembers = {},
|
||||
clearSelection = {},
|
||||
addContact = {},
|
||||
|
|
|
@ -74,9 +74,6 @@ fun ModalData.GroupChatInfoView(
|
|||
val chatItemTTL = remember(groupInfo.id) { mutableStateOf(if (groupInfo.chatItemTTL != null) ChatItemTTL.fromSeconds(groupInfo.chatItemTTL) else null) }
|
||||
val deletingItems = rememberSaveable(groupInfo.id) { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val activeSortedMembers = remember { chatModel.groupMembers }.value
|
||||
.filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
|
||||
.sortedByDescending { it.memberRole }
|
||||
|
||||
GroupChatInfoLayout(
|
||||
chat,
|
||||
|
@ -98,7 +95,9 @@ fun ModalData.GroupChatInfoView(
|
|||
|
||||
setChatTTLAlert(chatsCtx, chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems)
|
||||
},
|
||||
activeSortedMembers = activeSortedMembers,
|
||||
activeSortedMembers = remember { chatModel.groupMembers }.value
|
||||
.filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
|
||||
.sortedByDescending { it.memberRole },
|
||||
developerTools,
|
||||
onLocalAliasChanged = { setGroupAlias(chat, it, chatModel) },
|
||||
groupLink,
|
||||
|
@ -127,7 +126,7 @@ fun ModalData.GroupChatInfoView(
|
|||
}
|
||||
ModalManager.end.showModalCloseable(true) { closeCurrent ->
|
||||
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
|
||||
GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, closeCurrent) {
|
||||
GroupMemberInfoView(rhId, groupInfo, mem, stats, code, chatModel, closeCurrent) {
|
||||
closeCurrent()
|
||||
close()
|
||||
}
|
||||
|
@ -141,17 +140,6 @@ fun ModalData.GroupChatInfoView(
|
|||
addOrEditWelcomeMessage = {
|
||||
ModalManager.end.showCustomModal { close -> GroupWelcomeView(chatModel, rhId, groupInfo, close) }
|
||||
},
|
||||
openMemberSupport = {
|
||||
ModalManager.end.showCustomModal { close ->
|
||||
MemberSupportView(
|
||||
rhId,
|
||||
chat,
|
||||
groupInfo,
|
||||
scrollToItemId,
|
||||
close
|
||||
)
|
||||
}
|
||||
},
|
||||
openPreferences = {
|
||||
ModalManager.end.showCustomModal { close ->
|
||||
GroupPreferencesView(
|
||||
|
@ -329,40 +317,6 @@ fun AddGroupMembersButton(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserSupportChatButton(
|
||||
chat: Chat,
|
||||
groupInfo: GroupInfo,
|
||||
scrollToItemId: MutableState<Long?>
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
SettingsActionItemWithContent(
|
||||
painterResource(if (chat.supportUnreadCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag),
|
||||
stringResource(MR.strings.button_support_chat),
|
||||
click = {
|
||||
val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = null)
|
||||
val supportChatInfo = ChatInfo.Group(groupInfo, groupChatScope = scopeInfo)
|
||||
scope.launch {
|
||||
showMemberSupportChatView(
|
||||
chatModel.chatId,
|
||||
scrollToItemId = scrollToItemId,
|
||||
supportChatInfo,
|
||||
scopeInfo
|
||||
)
|
||||
}
|
||||
},
|
||||
iconColor = (if (chat.supportUnreadCount > 0) MaterialTheme.colors.primary else MaterialTheme.colors.secondary),
|
||||
) {
|
||||
if (chat.supportUnreadCount > 0) {
|
||||
UnreadBadge(
|
||||
text = unreadCountStr(chat.supportUnreadCount),
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModalData.GroupChatInfoLayout(
|
||||
chat: Chat,
|
||||
|
@ -383,7 +337,6 @@ fun ModalData.GroupChatInfoLayout(
|
|||
showMemberInfo: (GroupMember) -> Unit,
|
||||
editGroupProfile: () -> Unit,
|
||||
addOrEditWelcomeMessage: () -> Unit,
|
||||
openMemberSupport: () -> Unit,
|
||||
openPreferences: () -> Unit,
|
||||
deleteGroup: () -> Unit,
|
||||
clearChat: () -> Unit,
|
||||
|
@ -469,40 +422,6 @@ fun ModalData.GroupChatInfoLayout(
|
|||
|
||||
SectionSpacer()
|
||||
|
||||
var anyTopSectionRowShow = false
|
||||
SectionView {
|
||||
if (groupInfo.canAddMembers && groupInfo.businessChat == null) {
|
||||
anyTopSectionRowShow = true
|
||||
if (groupLink == null) {
|
||||
CreateGroupLinkButton(manageGroupLink)
|
||||
} else {
|
||||
GroupLinkButton(manageGroupLink)
|
||||
}
|
||||
}
|
||||
if (groupInfo.businessChat == null && groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
|
||||
anyTopSectionRowShow = true
|
||||
MemberSupportButton(chat, openMemberSupport)
|
||||
}
|
||||
if (groupInfo.canModerate) {
|
||||
anyTopSectionRowShow = true
|
||||
GroupReportsButton(chat) {
|
||||
scope.launch {
|
||||
showGroupReportsView(chatModel.chatId, scrollToItemId, chat.chatInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
groupInfo.membership.memberActive &&
|
||||
(groupInfo.membership.memberRole < GroupMemberRole.Moderator || groupInfo.membership.supportChat != null)
|
||||
) {
|
||||
anyTopSectionRowShow = true
|
||||
UserSupportChatButton(chat, groupInfo, scrollToItemId)
|
||||
}
|
||||
}
|
||||
if (anyTopSectionRowShow) {
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
}
|
||||
|
||||
SectionView {
|
||||
if (groupInfo.isOwner && groupInfo.businessChat?.chatType == null) {
|
||||
EditGroupProfileButton(editGroupProfile)
|
||||
|
@ -512,17 +431,19 @@ fun ModalData.GroupChatInfoLayout(
|
|||
}
|
||||
val prefsTitleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences
|
||||
GroupPreferencesButton(prefsTitleId, openPreferences)
|
||||
}
|
||||
val footerId = if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs else MR.strings.only_chat_owners_can_change_prefs
|
||||
SectionTextFooter(stringResource(footerId))
|
||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
|
||||
|
||||
SectionView {
|
||||
if (groupInfo.canModerate) {
|
||||
GroupReportsButton {
|
||||
scope.launch {
|
||||
showGroupReportsView(chatModel.chatId, scrollToItemId, chat.chatInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (activeSortedMembers.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) {
|
||||
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
|
||||
} else {
|
||||
SendReceiptsOptionDisabled()
|
||||
}
|
||||
|
||||
WallpaperButton {
|
||||
ModalManager.end.showModal {
|
||||
val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } }
|
||||
|
@ -532,13 +453,23 @@ fun ModalData.GroupChatInfoLayout(
|
|||
}
|
||||
}
|
||||
}
|
||||
ChatTTLOption(chatItemTTL, setChatItemTTL, deletingItems)
|
||||
SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer))
|
||||
}
|
||||
val footerId = if (groupInfo.businessChat == null) MR.strings.only_group_owners_can_change_prefs else MR.strings.only_chat_owners_can_change_prefs
|
||||
SectionTextFooter(stringResource(footerId))
|
||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
|
||||
|
||||
ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems)
|
||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = true)
|
||||
|
||||
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1)) {
|
||||
if (groupInfo.canAddMembers) {
|
||||
if (groupInfo.businessChat == null) {
|
||||
if (groupLink == null) {
|
||||
CreateGroupLinkButton(manageGroupLink)
|
||||
} else {
|
||||
GroupLinkButton(manageGroupLink)
|
||||
}
|
||||
}
|
||||
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
|
||||
val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
val addMembersTitleId = when (groupInfo.businessChat?.chatType) {
|
||||
|
@ -550,7 +481,7 @@ fun ModalData.GroupChatInfoLayout(
|
|||
}
|
||||
if (activeSortedMembers.size > 8) {
|
||||
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
|
||||
MemberListSearchRowView(searchText)
|
||||
SearchRowView(searchText)
|
||||
}
|
||||
}
|
||||
SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
|
@ -596,7 +527,7 @@ fun ModalData.GroupChatInfoLayout(
|
|||
val titleId = if (groupInfo.businessChat == null) MR.strings.button_delete_group else MR.strings.button_delete_chat
|
||||
DeleteGroupButton(titleId, deleteGroup)
|
||||
}
|
||||
if (groupInfo.membership.memberCurrentOrPending) {
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
val titleId = if (groupInfo.businessChat == null) MR.strings.button_leave_group else MR.strings.button_leave_chat
|
||||
LeaveGroupButton(titleId, leaveGroup)
|
||||
}
|
||||
|
@ -700,14 +631,17 @@ private fun SelectedItemsCounterToolbarSetter(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun ChatTTLOption(chatItemTTL: State<ChatItemTTL?>, setChatItemTTL: (ChatItemTTL?) -> Unit, deletingItems: State<Boolean>) {
|
||||
fun ChatTTLSection(chatItemTTL: State<ChatItemTTL?>, setChatItemTTL: (ChatItemTTL?) -> Unit, deletingItems: State<Boolean>) {
|
||||
Box {
|
||||
TtlOptions(
|
||||
chatItemTTL,
|
||||
enabled = remember { derivedStateOf { !deletingItems.value } },
|
||||
onSelected = setChatItemTTL,
|
||||
default = chatModel.chatItemTTL
|
||||
)
|
||||
SectionView {
|
||||
TtlOptions(
|
||||
chatItemTTL,
|
||||
enabled = remember { derivedStateOf { !deletingItems.value } },
|
||||
onSelected = setChatItemTTL,
|
||||
default = chatModel.chatItemTTL
|
||||
)
|
||||
SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer))
|
||||
}
|
||||
if (deletingItems.value) {
|
||||
Box(Modifier.matchParentSize()) {
|
||||
ProgressIndicator()
|
||||
|
@ -749,23 +683,6 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo, groupInfo: GroupInfo) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemberSupportButton(chat: Chat, onClick: () -> Unit) {
|
||||
SettingsActionItemWithContent(
|
||||
painterResource(if (chat.supportUnreadCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag),
|
||||
stringResource(MR.strings.member_support),
|
||||
click = onClick,
|
||||
iconColor = (if (chat.supportUnreadCount > 0) MaterialTheme.colors.primary else MaterialTheme.colors.secondary)
|
||||
) {
|
||||
if (chat.supportUnreadCount > 0) {
|
||||
UnreadBadge(
|
||||
text = unreadCountStr(chat.supportUnreadCount),
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GroupPreferencesButton(titleId: StringResource, onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
|
@ -776,20 +693,12 @@ private fun GroupPreferencesButton(titleId: StringResource, onClick: () -> Unit)
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun GroupReportsButton(chat: Chat, onClick: () -> Unit) {
|
||||
SettingsActionItemWithContent(
|
||||
painterResource(if (chat.chatStats.reportsCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag),
|
||||
private fun GroupReportsButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_flag),
|
||||
stringResource(MR.strings.group_reports_member_reports),
|
||||
click = onClick,
|
||||
iconColor = (if (chat.chatStats.reportsCount > 0) Color.Red else MaterialTheme.colors.secondary)
|
||||
) {
|
||||
if (chat.chatStats.reportsCount > 0) {
|
||||
UnreadBadge(
|
||||
text = unreadCountStr(chat.chatStats.reportsCount),
|
||||
backgroundColor = Color.Red
|
||||
)
|
||||
}
|
||||
}
|
||||
click = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
@ -911,7 +820,7 @@ fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = tr
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun MemberVerifiedShield() {
|
||||
private fun MemberVerifiedShield() {
|
||||
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.padding(end = 3.dp).size(16.dp), tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
|
||||
|
@ -1032,7 +941,7 @@ private fun DeleteGroupButton(titleId: StringResource, onClick: () -> Unit) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun MemberListSearchRowView(
|
||||
private fun SearchRowView(
|
||||
searchText: MutableState<TextFieldValue> = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
|
||||
) {
|
||||
Box(Modifier.width(36.dp), contentAlignment = Alignment.Center) {
|
||||
|
@ -1055,18 +964,16 @@ private fun setGroupAlias(chat: Chat, localAlias: String, chatModel: ChatModel)
|
|||
|
||||
fun removeMembers(rhId: Long?, groupInfo: GroupInfo, memberIds: List<Long>, onSuccess: () -> Unit = {}) {
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, memberIds)
|
||||
if (r != null) {
|
||||
val (updatedGroupInfo, updatedMembers) = r
|
||||
val updatedMembers = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, memberIds)
|
||||
if (updatedMembers != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateGroup(rhId, updatedGroupInfo)
|
||||
updatedMembers.forEach { updatedMember ->
|
||||
chatModel.chatsContext.upsertGroupMember(rhId, updatedGroupInfo, updatedMember)
|
||||
chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
updatedMembers.forEach { updatedMember ->
|
||||
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, updatedGroupInfo, updatedMember)
|
||||
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
onSuccess()
|
||||
|
@ -1109,18 +1016,7 @@ fun PreviewGroupChatInfoLayout() {
|
|||
selectedItems = remember { mutableStateOf(null) },
|
||||
appBar = remember { mutableStateOf(null) },
|
||||
scrollToItemId = remember { mutableStateOf(null) },
|
||||
addMembers = {},
|
||||
showMemberInfo = {},
|
||||
editGroupProfile = {},
|
||||
addOrEditWelcomeMessage = {},
|
||||
openMemberSupport = {},
|
||||
openPreferences = {},
|
||||
deleteGroup = {},
|
||||
clearChat = {},
|
||||
leaveGroup = {},
|
||||
manageGroupLink = {},
|
||||
onSearchClicked = {},
|
||||
deletingItems = remember { mutableStateOf(true) }
|
||||
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, onSearchClicked = {}, deletingItems = remember { mutableStateOf(true) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,6 @@ fun GroupMemberInfoView(
|
|||
rhId: Long?,
|
||||
groupInfo: GroupInfo,
|
||||
member: GroupMember,
|
||||
scrollToItemId: MutableState<Long?>,
|
||||
connectionStats: ConnectionStats?,
|
||||
connectionCode: String?,
|
||||
chatModel: ChatModel,
|
||||
|
@ -80,7 +79,6 @@ fun GroupMemberInfoView(
|
|||
rhId = rhId,
|
||||
groupInfo,
|
||||
member,
|
||||
scrollToItemId,
|
||||
connStats,
|
||||
newRole,
|
||||
developerTools,
|
||||
|
@ -246,18 +244,16 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c
|
|||
confirmText = generalGetString(MR.strings.remove_member_confirmation),
|
||||
onConfirm = {
|
||||
withBGApi {
|
||||
val r = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId))
|
||||
if (r != null) {
|
||||
val (updatedGroupInfo, removedMembers) = r
|
||||
val removedMembers = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId))
|
||||
if (removedMembers != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateGroup(rhId, updatedGroupInfo)
|
||||
removedMembers.forEach { removedMember ->
|
||||
chatModel.chatsContext.upsertGroupMember(rhId, updatedGroupInfo, removedMember)
|
||||
chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, removedMember)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
removedMembers.forEach { removedMember ->
|
||||
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, updatedGroupInfo, removedMember)
|
||||
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, removedMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -273,7 +269,6 @@ fun GroupMemberInfoLayout(
|
|||
rhId: Long?,
|
||||
groupInfo: GroupInfo,
|
||||
member: GroupMember,
|
||||
scrollToItemId: MutableState<Long?>,
|
||||
connStats: MutableState<ConnectionStats?>,
|
||||
newRole: MutableState<GroupMemberRole>,
|
||||
developerTools: Boolean,
|
||||
|
@ -304,29 +299,6 @@ fun GroupMemberInfoLayout(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SupportChatButton() {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_flag),
|
||||
stringResource(MR.strings.button_support_chat_member),
|
||||
click = {
|
||||
val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = member)
|
||||
val supportChatInfo = ChatInfo.Group(groupInfo, groupChatScope = scopeInfo)
|
||||
scope.launch {
|
||||
showMemberSupportChatView(
|
||||
chatModel.chatId,
|
||||
scrollToItemId = scrollToItemId,
|
||||
supportChatInfo,
|
||||
scopeInfo
|
||||
)
|
||||
}
|
||||
},
|
||||
iconColor = MaterialTheme.colors.secondary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ModeratorDestructiveSection() {
|
||||
val canBlockForAll = member.canBlockForAll(groupInfo)
|
||||
|
@ -441,12 +413,6 @@ fun GroupMemberInfoLayout(
|
|||
|
||||
if (member.memberActive) {
|
||||
SectionView {
|
||||
if (
|
||||
groupInfo.membership.memberRole >= GroupMemberRole.Moderator &&
|
||||
(member.memberRole < GroupMemberRole.Moderator || member.supportChat != null)
|
||||
) {
|
||||
SupportChatButton()
|
||||
}
|
||||
if (connectionCode != null) {
|
||||
VerifyCodeButton(member.verified, verifyClicked)
|
||||
}
|
||||
|
@ -912,7 +878,6 @@ fun PreviewGroupMemberInfoLayout() {
|
|||
rhId = null,
|
||||
groupInfo = GroupInfo.sampleData,
|
||||
member = GroupMember.sampleData,
|
||||
scrollToItemId = remember { mutableStateOf(null) },
|
||||
connStats = remember { mutableStateOf(null) },
|
||||
newRole = remember { mutableStateOf(GroupMemberRole.Member) },
|
||||
developerTools = false,
|
||||
|
|
|
@ -44,7 +44,7 @@ fun SelectedItemsMembersToolbar(
|
|||
) {
|
||||
// It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty
|
||||
Box(Modifier.alpha(0f)) {
|
||||
ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() })
|
||||
ComposeView(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() })
|
||||
}
|
||||
Row(
|
||||
Modifier
|
||||
|
|
|
@ -35,7 +35,6 @@ private val MAX_PICKER_HEIGHT = (PICKER_ROW_SIZE * 4) + (MEMBER_ROW_AVATAR_SIZE
|
|||
|
||||
@Composable
|
||||
fun GroupMentions(
|
||||
chatsCtx: ChatModel.ChatsContext,
|
||||
rhId: Long?,
|
||||
composeState: MutableState<ComposeState>,
|
||||
composeViewFocusRequester: FocusRequester?,
|
||||
|
@ -49,31 +48,12 @@ fun GroupMentions(
|
|||
val mentionName = remember { mutableStateOf("") }
|
||||
val mentionRange = remember { mutableStateOf<TextRange?>(null) }
|
||||
val mentionMemberId = remember { mutableStateOf<String?>(null) }
|
||||
|
||||
fun contextMemberFilter(member: GroupMember): Boolean =
|
||||
when (chatsCtx.secondaryContextFilter) {
|
||||
null -> true
|
||||
is SecondaryContextFilter.GroupChatScopeContext ->
|
||||
when (chatsCtx.secondaryContextFilter.groupScopeInfo) {
|
||||
is GroupChatScopeInfo.MemberSupport -> {
|
||||
val scopeMember = chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_
|
||||
if (scopeMember != null) {
|
||||
member.memberRole >= GroupMemberRole.Moderator || member.groupMemberId == scopeMember.groupMemberId
|
||||
} else {
|
||||
member.memberRole >= GroupMemberRole.Moderator
|
||||
}
|
||||
}
|
||||
}
|
||||
is SecondaryContextFilter.MsgContentTagContext -> false
|
||||
}
|
||||
|
||||
val filteredMembers = remember {
|
||||
derivedStateOf {
|
||||
val members = chatModel.groupMembers.value
|
||||
.filter {
|
||||
val status = it.memberStatus
|
||||
status != GroupMemberStatus.MemLeft && status != GroupMemberStatus.MemRemoved && status != GroupMemberStatus.MemInvited
|
||||
&& contextMemberFilter(it)
|
||||
}
|
||||
.sortedByDescending { it.memberRole }
|
||||
|
||||
|
|
|
@ -17,9 +17,7 @@ import chat.simplex.common.views.usersettings.PreferenceToggleWithIcon
|
|||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItem
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
private val featureRoles: List<Pair<GroupMemberRole?, String>> = listOf(
|
||||
|
@ -73,16 +71,6 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () ->
|
|||
preferences = currentPreferences
|
||||
},
|
||||
savePrefs = ::savePrefs,
|
||||
openMemberAdmission = {
|
||||
ModalManager.end.showCustomModal { close ->
|
||||
MemberAdmissionView(
|
||||
chatModel,
|
||||
rhId,
|
||||
chatId,
|
||||
close
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -95,15 +83,10 @@ private fun GroupPreferencesLayout(
|
|||
applyPrefs: (FullGroupPreferences) -> Unit,
|
||||
reset: () -> Unit,
|
||||
savePrefs: () -> Unit,
|
||||
openMemberAdmission: () -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
val titleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences
|
||||
AppBarTitle(stringResource(titleId))
|
||||
if (groupInfo.businessChat == null) {
|
||||
MemberAdmissionButton(openMemberAdmission)
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
}
|
||||
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) }
|
||||
val onTTLUpdated = { ttl: Int? ->
|
||||
applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl)))
|
||||
|
@ -173,15 +156,6 @@ private fun GroupPreferencesLayout(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemberAdmissionButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_toggle_on),
|
||||
stringResource(MR.strings.member_admission),
|
||||
click = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeatureSection(
|
||||
feature: GroupFeature,
|
||||
|
|
|
@ -15,15 +15,7 @@ import dev.icerock.moko.resources.compose.stringResource
|
|||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@Composable
|
||||
private fun GroupReportsView(
|
||||
reportsChatsCtx: ChatModel.ChatsContext,
|
||||
staleChatId: State<String?>,
|
||||
scrollToItemId: MutableState<Long?>,
|
||||
close: () -> Unit
|
||||
) {
|
||||
KeyChangeEffect(chatModel.chatId.value) {
|
||||
close()
|
||||
}
|
||||
private fun GroupReportsView(reportsChatsCtx: ChatModel.ChatsContext, staleChatId: State<String?>, scrollToItemId: MutableState<Long?>) {
|
||||
ChatView(reportsChatsCtx, staleChatId, scrollToItemId, onComposed = {})
|
||||
}
|
||||
|
||||
|
@ -61,7 +53,7 @@ fun GroupReportsAppBar(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) {
|
||||
private fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) {
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { chatModel.chatId.value }
|
||||
.distinctUntilChanged()
|
||||
|
@ -77,13 +69,13 @@ fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) {
|
|||
}
|
||||
|
||||
suspend fun showGroupReportsView(staleChatId: State<String?>, scrollToItemId: MutableState<Long?>, chatInfo: ChatInfo) {
|
||||
val reportsChatsCtx = ChatModel.ChatsContext(secondaryContextFilter = SecondaryContextFilter.MsgContentTagContext(MsgContentTag.Report))
|
||||
val reportsChatsCtx = ChatModel.ChatsContext(contentTag = MsgContentTag.Report)
|
||||
openChat(secondaryChatsCtx = reportsChatsCtx, chatModel.remoteHostId(), chatInfo)
|
||||
ModalManager.end.showCustomModal(true, id = ModalViewId.SECONDARY_CHAT) { close ->
|
||||
ModalView({}, showAppBar = false) {
|
||||
val chatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chatModel.chatId.value }?.chatInfo } }.value
|
||||
if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate) {
|
||||
GroupReportsView(reportsChatsCtx, staleChatId, scrollToItemId, close)
|
||||
GroupReportsView(reportsChatsCtx, staleChatId, scrollToItemId)
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
close()
|
||||
|
|
|
@ -1,152 +0,0 @@
|
|||
package chat.simplex.common.views.chat.group
|
||||
|
||||
import InfoRow
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.ColumnWithScrollBar
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun MemberAdmissionView(m: ChatModel, rhId: Long?, chatId: String, close: () -> Unit) {
|
||||
val groupInfo = remember { derivedStateOf {
|
||||
val ch = m.getChat(chatId)
|
||||
val g = (ch?.chatInfo as? ChatInfo.Group)?.groupInfo
|
||||
if (g == null || ch.remoteHostId != rhId) null else g
|
||||
}}
|
||||
val gInfo = groupInfo.value ?: return
|
||||
var admission by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(gInfo.groupProfile.memberAdmission) }
|
||||
var currentAdmission by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(admission) }
|
||||
|
||||
fun saveAdmission(afterSave: () -> Unit = {}) {
|
||||
withBGApi {
|
||||
val gp = gInfo.groupProfile.copy(memberAdmission = admission)
|
||||
val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp)
|
||||
if (g != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateGroup(rhId, g)
|
||||
currentAdmission = admission
|
||||
}
|
||||
}
|
||||
afterSave()
|
||||
}
|
||||
}
|
||||
ModalView(
|
||||
close = {
|
||||
if (admission == currentAdmission) close()
|
||||
else showUnsavedChangesAlert({ saveAdmission(close) }, close)
|
||||
},
|
||||
) {
|
||||
MemberAdmissionLayout(
|
||||
admission,
|
||||
currentAdmission,
|
||||
gInfo,
|
||||
applyAdmission = { admsn ->
|
||||
admission = admsn
|
||||
},
|
||||
reset = {
|
||||
admission = currentAdmission
|
||||
},
|
||||
saveAdmission = ::saveAdmission,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemberAdmissionLayout(
|
||||
admission: GroupMemberAdmission?,
|
||||
currentAdmission: GroupMemberAdmission?,
|
||||
groupInfo: GroupInfo,
|
||||
applyAdmission: (GroupMemberAdmission) -> Unit,
|
||||
reset: () -> Unit,
|
||||
saveAdmission: () -> Unit,
|
||||
) {
|
||||
ColumnWithScrollBar {
|
||||
AppBarTitle(stringResource(MR.strings.member_admission))
|
||||
val review = remember(admission) { mutableStateOf(admission?.review) }
|
||||
AdmissionSection(MR.strings.admission_stage_review, MR.strings.admission_stage_review_descr, review, groupInfo) { criteria ->
|
||||
if (admission != null) {
|
||||
applyAdmission(admission.copy(review = criteria))
|
||||
} else {
|
||||
applyAdmission(GroupMemberAdmission(review = criteria))
|
||||
}
|
||||
}
|
||||
if (groupInfo.isOwner) {
|
||||
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
|
||||
ResetSaveButtons(
|
||||
reset = reset,
|
||||
save = saveAdmission,
|
||||
disabled = admission == currentAdmission
|
||||
)
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
|
||||
private val memberCriterias: List<Pair<MemberCriteria?, String>> = listOf(
|
||||
null to generalGetString(MR.strings.member_criteria_off),
|
||||
MemberCriteria.All to generalGetString(MR.strings.member_criteria_all)
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun AdmissionSection(
|
||||
admissionStageStrId: StringResource,
|
||||
admissionStageDescrStrId: StringResource,
|
||||
memberCriteria: State<MemberCriteria?>,
|
||||
groupInfo: GroupInfo,
|
||||
onSelected: (MemberCriteria?) -> Unit
|
||||
) {
|
||||
SectionView {
|
||||
if (groupInfo.isOwner) {
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(admissionStageStrId),
|
||||
memberCriterias,
|
||||
memberCriteria,
|
||||
onSelected = { value ->
|
||||
onSelected(value)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
InfoRow(
|
||||
stringResource(admissionStageStrId),
|
||||
memberCriteria.value?.text ?: generalGetString(MR.strings.member_criteria_off)
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(stringResource( admissionStageDescrStrId))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) {
|
||||
SectionView {
|
||||
SectionItemView(reset, disabled = disabled) {
|
||||
Text(stringResource(MR.strings.reset_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView(save, disabled = disabled) {
|
||||
Text(stringResource(MR.strings.save_and_notify_group_members), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(MR.strings.save_admission_question),
|
||||
confirmText = generalGetString(MR.strings.save_and_notify_group_members),
|
||||
dismissText = generalGetString(MR.strings.exit_without_saving),
|
||||
onConfirm = save,
|
||||
onDismiss = revert,
|
||||
)
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
package chat.simplex.common.views.chat.group
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
private fun MemberSupportChatView(
|
||||
chatInfo: ChatInfo,
|
||||
memberSupportChatsCtx: ChatModel.ChatsContext,
|
||||
staleChatId: State<String?>,
|
||||
scrollToItemId: MutableState<Long?>
|
||||
) {
|
||||
KeyChangeEffect(chatModel.chatId.value) {
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatInfo.id }
|
||||
if (
|
||||
memberSupportChatsCtx.isUserSupportChat
|
||||
&& chat?.chatInfo?.groupInfo_?.membership?.memberPending == true
|
||||
) {
|
||||
withBGApi {
|
||||
chatModel.chatId.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ChatView(memberSupportChatsCtx, staleChatId, scrollToItemId, onComposed = {})
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MemberSupportChatAppBar(
|
||||
chatsCtx: ChatModel.ChatsContext,
|
||||
scopeMember_: GroupMember?,
|
||||
close: () -> Unit,
|
||||
onSearchValueChanged: (String) -> Unit
|
||||
) {
|
||||
val oneHandUI = remember { ChatController.appPrefs.oneHandUI.state }
|
||||
val showSearch = rememberSaveable { mutableStateOf(false) }
|
||||
val onBackClicked = {
|
||||
if (!showSearch.value) {
|
||||
close()
|
||||
} else {
|
||||
onSearchValueChanged("")
|
||||
showSearch.value = false
|
||||
}
|
||||
}
|
||||
BackHandler(onBack = onBackClicked)
|
||||
if (scopeMember_ != null) {
|
||||
DefaultAppBar(
|
||||
navigationButton = { NavigationButtonBack(onBackClicked) },
|
||||
title = { MemberSupportChatToolbarTitle(scopeMember_) },
|
||||
onTitleClick = null,
|
||||
onTop = !oneHandUI.value,
|
||||
showSearch = showSearch.value,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
buttons = {
|
||||
IconButton({ showSearch.value = true }) {
|
||||
Icon(painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
DefaultAppBar(
|
||||
navigationButton = { NavigationButtonBack(onBackClicked) },
|
||||
fixedTitleText = stringResource(MR.strings.support_chat),
|
||||
onTitleClick = null,
|
||||
onTop = !oneHandUI.value,
|
||||
showSearch = showSearch.value,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
buttons = {
|
||||
IconButton({ showSearch.value = true }) {
|
||||
Icon(painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
ItemsReload(chatsCtx)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MemberSupportChatToolbarTitle(member: GroupMember, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
MemberProfileImage(size = imageSize * fontSizeSqrtMultiplier, member, iconColor)
|
||||
Column(
|
||||
Modifier.padding(start = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (member.verified) {
|
||||
MemberVerifiedShield()
|
||||
}
|
||||
Text(
|
||||
member.displayName, fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (member.fullName != "" && member.fullName != member.displayName && member.localAlias.isEmpty()) {
|
||||
Text(
|
||||
member.fullName,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun showMemberSupportChatView(staleChatId: State<String?>, scrollToItemId: MutableState<Long?>, chatInfo: ChatInfo, scopeInfo: GroupChatScopeInfo) {
|
||||
val memberSupportChatsCtx = ChatModel.ChatsContext(secondaryContextFilter = SecondaryContextFilter.GroupChatScopeContext(scopeInfo))
|
||||
openChat(secondaryChatsCtx = memberSupportChatsCtx, chatModel.remoteHostId(), chatInfo)
|
||||
ModalManager.end.showCustomModal(true, id = ModalViewId.SECONDARY_CHAT) { close ->
|
||||
ModalView({}, showAppBar = false) {
|
||||
if (chatInfo is ChatInfo.Group && chatInfo.groupChatScope != null) {
|
||||
MemberSupportChatView(chatInfo, memberSupportChatsCtx, staleChatId, scrollToItemId)
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,282 +0,0 @@
|
|||
package chat.simplex.common.views.chat.group
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionItemView
|
||||
import SectionItemViewLongClickable
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.chatlist.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ModalData.MemberSupportView(
|
||||
rhId: Long?,
|
||||
chat: Chat,
|
||||
groupInfo: GroupInfo,
|
||||
scrollToItemId: MutableState<Long?>,
|
||||
close: () -> Unit
|
||||
) {
|
||||
KeyChangeEffect(chatModel.chatId.value) {
|
||||
ModalManager.end.closeModals()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
setGroupMembers(rhId, groupInfo, chatModel)
|
||||
}
|
||||
ModalView(
|
||||
close = close,
|
||||
endButtons = { RefreshMembersButton(rhId, groupInfo) }
|
||||
) {
|
||||
MemberSupportViewLayout(
|
||||
chat,
|
||||
groupInfo,
|
||||
scrollToItemId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RefreshMembersButton(
|
||||
rhId: Long?,
|
||||
groupInfo: GroupInfo
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
withBGApi {
|
||||
setGroupMembers(rhId, groupInfo, chatModel)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_refresh),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModalData.MemberSupportViewLayout(
|
||||
chat: Chat,
|
||||
groupInfo: GroupInfo,
|
||||
scrollToItemId: MutableState<Long?>
|
||||
) {
|
||||
val oneHandUI = remember { ChatController.appPrefs.oneHandUI.state }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val membersWithChats = remember { chatModel.groupMembers }.value
|
||||
.filter { it.supportChat != null && it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
|
||||
.sortedWith(
|
||||
compareByDescending<GroupMember> { it.memberPending }
|
||||
.thenByDescending { (it.supportChat?.mentions ?: 0) > 0 }
|
||||
.thenByDescending { (it.supportChat?.memberAttention ?: 0) > 0 }
|
||||
.thenByDescending { (it.supportChat?.unread ?: 0) > 0 }
|
||||
.thenByDescending { it.supportChat?.chatTs }
|
||||
)
|
||||
|
||||
val searchText = remember { stateGetOrPut("searchText") { TextFieldValue() } }
|
||||
val filteredmembersWithChats = remember(membersWithChats) {
|
||||
derivedStateOf {
|
||||
val s = searchText.value.text.trim().lowercase()
|
||||
if (s.isEmpty()) membersWithChats else membersWithChats.filter { m -> m.anyNameContains(s) }
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumnWithScrollBar(
|
||||
contentPadding =
|
||||
PaddingValues(
|
||||
top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() + DEFAULT_PADDING + 5.dp else topPaddingToContent(false)
|
||||
)
|
||||
) {
|
||||
item {
|
||||
AppBarTitle(stringResource(MR.strings.member_support))
|
||||
}
|
||||
|
||||
if (membersWithChats.isEmpty()) {
|
||||
item {
|
||||
Box(Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING), contentAlignment = Alignment.Center) {
|
||||
Text(generalGetString(MR.strings.no_support_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
|
||||
MemberListSearchRowView(searchText)
|
||||
}
|
||||
}
|
||||
items(filteredmembersWithChats.value, key = { it.groupMemberId }) { member ->
|
||||
Divider()
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
SectionItemViewLongClickable(
|
||||
click = {
|
||||
val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = member)
|
||||
val supportChatInfo = ChatInfo.Group(groupInfo, groupChatScope = scopeInfo)
|
||||
scope.launch {
|
||||
showMemberSupportChatView(
|
||||
chatModel.chatId,
|
||||
scrollToItemId = scrollToItemId,
|
||||
supportChatInfo,
|
||||
scopeInfo
|
||||
)
|
||||
}
|
||||
},
|
||||
longClick = { showMenu.value = true },
|
||||
minHeight = 54.dp,
|
||||
padding = PaddingValues(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
DropDownMenuForSupportChat(chat.remoteHostId, member, groupInfo, showMenu)
|
||||
SupportChatRow(member)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Divider()
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SupportChatRow(member: GroupMember) {
|
||||
fun memberStatus(): String {
|
||||
return if (member.activeConn?.connDisabled == true) {
|
||||
generalGetString(MR.strings.member_info_member_disabled)
|
||||
} else if (member.activeConn?.connInactive == true) {
|
||||
generalGetString(MR.strings.member_info_member_inactive)
|
||||
} else if (member.memberPending) {
|
||||
member.memberStatus.text
|
||||
} else {
|
||||
member.memberRole.text
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SupportChatUnreadIndicator(supportChat: GroupSupportChat) {
|
||||
Box(Modifier.widthIn(min = 34.sp.toDp()), contentAlignment = Alignment.TopEnd) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.sp.toDp())) {
|
||||
if (supportChat.unread > 0 || supportChat.mentions > 0 || supportChat.memberAttention > 0) {
|
||||
val unreadBadgeColor = when {
|
||||
supportChat.mentions > 0 || supportChat.memberAttention > 0 -> MaterialTheme.colors.primaryVariant
|
||||
else -> MaterialTheme.colors.secondary
|
||||
}
|
||||
if (supportChat.mentions == 1 && supportChat.unread == 1) {
|
||||
Box(modifier = Modifier.offset(y = 2.sp.toDp()).size(15.sp.toDp()).background(unreadBadgeColor, shape = CircleShape), contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_alternate_email),
|
||||
contentDescription = generalGetString(MR.strings.notifications),
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(9.sp.toDp())
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (supportChat.mentions > 0 && supportChat.unread > 1) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_alternate_email),
|
||||
contentDescription = generalGetString(MR.strings.notifications),
|
||||
tint = unreadBadgeColor,
|
||||
modifier = Modifier.size(12.sp.toDp()).offset(y = 2.sp.toDp())
|
||||
)
|
||||
}
|
||||
|
||||
UnreadBadge(
|
||||
text = unreadCountStr(supportChat.unread),
|
||||
backgroundColor = unreadBadgeColor,
|
||||
yOffset = 2.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
Modifier.weight(1f).padding(top = MEMBER_ROW_VERTICAL_PADDING, end = DEFAULT_PADDING, bottom = MEMBER_ROW_VERTICAL_PADDING),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
MemberProfileImage(size = MEMBER_ROW_AVATAR_SIZE, member)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (member.verified) {
|
||||
MemberVerifiedShield()
|
||||
}
|
||||
Text(
|
||||
member.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
color = if (member.memberIncognito) Indigo else Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
memberStatus(),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
fontSize = 12.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
if (member.memberPending) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_flag_filled),
|
||||
contentDescription = null,
|
||||
Modifier.padding(end = 3.dp).size(16.dp),
|
||||
tint = MaterialTheme.colors.primaryVariant
|
||||
)
|
||||
}
|
||||
if (member.supportChat != null) {
|
||||
SupportChatUnreadIndicator(member.supportChat)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DropDownMenuForSupportChat(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState<Boolean>) {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (member.memberPending) {
|
||||
ItemAction(stringResource(MR.strings.accept_pending_member_button), painterResource(MR.images.ic_check), color = MaterialTheme.colors.primary, onClick = {
|
||||
acceptMemberDialog(rhId, groupInfo, member)
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
ItemAction(stringResource(MR.strings.remove_pending_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
|
||||
removeMemberDialog(rhId, groupInfo, member, chatModel)
|
||||
showMenu.value = false
|
||||
})
|
||||
// TODO [knocking] mark read, mark unread
|
||||
// ItemAction(stringResource(MR.strings.mark_unread), painterResource(MR.images.ic_mark_chat_unread), onClick = {
|
||||
// showMenu.value = false
|
||||
// })
|
||||
}
|
||||
}
|
|
@ -86,7 +86,6 @@ fun ChatItemView(
|
|||
joinGroup: (Long, () -> Unit) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
scrollToItem: (Long) -> Unit,
|
||||
scrollToItemId: MutableState<Long?>,
|
||||
scrollToQuotedItemFromItem: (Long) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
|
@ -272,7 +271,6 @@ fun ChatItemView(
|
|||
}
|
||||
}
|
||||
|
||||
// improvement could be to track "forwarded from" scope and open it
|
||||
@Composable
|
||||
fun GoToItemButton(alignStart: Boolean, parentActivated: State<Boolean>) {
|
||||
val chatTypeApiIdMsgId = cItem.meta.itemForwarded?.chatTypeApiIdMsgId
|
||||
|
@ -326,7 +324,7 @@ fun ChatItemView(
|
|||
) {
|
||||
@Composable
|
||||
fun framedItemView() {
|
||||
FramedItemView(chatsCtx, cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem, scrollToItemId, scrollToQuotedItemFromItem)
|
||||
FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem, scrollToQuotedItemFromItem)
|
||||
}
|
||||
|
||||
fun deleteMessageQuestionText(): String {
|
||||
|
@ -637,15 +635,6 @@ fun ChatItemView(
|
|||
CIEventView(eventItemViewText(reversedChatItems))
|
||||
}
|
||||
|
||||
@Composable fun PendingReviewEventItemView() {
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(chatEventStyle.copy(fontWeight = FontWeight.Bold)) { append(cItem.content.text) }
|
||||
},
|
||||
Modifier.padding(horizontal = 6.dp, vertical = 6.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeletedItem() {
|
||||
MarkedDeletedItemView(chatsCtx, cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
|
@ -722,16 +711,12 @@ fun ChatItemView(
|
|||
is CIContent.RcvGroupEventContent -> {
|
||||
when (c.rcvGroupEvent) {
|
||||
is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat)
|
||||
is RcvGroupEvent.NewMemberPendingReview -> PendingReviewEventItemView()
|
||||
else -> EventItemView()
|
||||
}
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
is CIContent.SndGroupEventContent -> {
|
||||
when (c.sndGroupEvent) {
|
||||
is SndGroupEvent.UserPendingReview -> PendingReviewEventItemView()
|
||||
else -> EventItemView()
|
||||
}
|
||||
EventItemView()
|
||||
MsgContentItemDropdownMenu()
|
||||
}
|
||||
is CIContent.RcvConnEventContent -> {
|
||||
|
@ -1437,7 +1422,7 @@ fun PreviewChatItemView(
|
|||
chatItem: ChatItem = ChatItem.getSampleData(1, CIDirection.DirectSnd(), Clock.System.now(), "hello")
|
||||
) {
|
||||
ChatItemView(
|
||||
chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null),
|
||||
chatsCtx = ChatModel.ChatsContext(contentTag = null),
|
||||
rhId = null,
|
||||
ChatInfo.Direct.sampleData,
|
||||
chatItem,
|
||||
|
@ -1459,7 +1444,6 @@ fun PreviewChatItemView(
|
|||
joinGroup = { _, _ -> },
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
scrollToItemId = remember { mutableStateOf(null) },
|
||||
scrollToQuotedItemFromItem = {},
|
||||
acceptFeature = { _, _, _ -> },
|
||||
openDirectChat = { _ -> },
|
||||
|
@ -1488,7 +1472,7 @@ fun PreviewChatItemView(
|
|||
fun PreviewChatItemViewDeletedContent() {
|
||||
SimpleXTheme {
|
||||
ChatItemView(
|
||||
chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null),
|
||||
chatsCtx = ChatModel.ChatsContext(contentTag = null),
|
||||
rhId = null,
|
||||
ChatInfo.Direct.sampleData,
|
||||
ChatItem.getDeletedContentSampleData(),
|
||||
|
@ -1510,7 +1494,6 @@ fun PreviewChatItemViewDeletedContent() {
|
|||
joinGroup = { _, _ -> },
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
scrollToItemId = remember { mutableStateOf(null) },
|
||||
scrollToQuotedItemFromItem = {},
|
||||
acceptFeature = { _, _, _ -> },
|
||||
openDirectChat = { _ -> },
|
||||
|
|
|
@ -27,7 +27,6 @@ import kotlin.math.ceil
|
|||
|
||||
@Composable
|
||||
fun FramedItemView(
|
||||
chatsCtx: ChatModel.ChatsContext,
|
||||
chatInfo: ChatInfo,
|
||||
ci: ChatItem,
|
||||
uriHandler: UriHandler? = null,
|
||||
|
@ -40,7 +39,6 @@ fun FramedItemView(
|
|||
receiveFile: (Long) -> Unit,
|
||||
onLinkLongClick: (link: String) -> Unit = {},
|
||||
scrollToItem: (Long) -> Unit = {},
|
||||
scrollToItemId: MutableState<Long?>,
|
||||
scrollToQuotedItemFromItem: (Long) -> Unit = {},
|
||||
) {
|
||||
val sent = ci.chatDir.sent
|
||||
|
@ -255,11 +253,7 @@ fun FramedItemView(
|
|||
onLongClick = { showMenu.value = true },
|
||||
onClick = {
|
||||
if (ci.quotedItem.itemId != null) {
|
||||
if (ci.isReport && chatsCtx.secondaryContextFilter != null) {
|
||||
scrollToItemId.value = ci.quotedItem.itemId
|
||||
} else {
|
||||
scrollToItem(ci.quotedItem.itemId)
|
||||
}
|
||||
scrollToItem(ci.quotedItem.itemId)
|
||||
} else {
|
||||
scrollToQuotedItemFromItem(ci.id)
|
||||
}
|
||||
|
|
|
@ -236,6 +236,7 @@ suspend fun openChat(
|
|||
|
||||
suspend fun openLoadedChat(chat: Chat) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.chatItemStatuses.clear()
|
||||
chatModel.chatsContext.chatItems.replaceAll(chat.chatItems)
|
||||
chatModel.chatId.value = chat.chatInfo.id
|
||||
chatModel.chatsContext.chatState.clear()
|
||||
|
@ -304,7 +305,7 @@ fun GroupMenuItems(
|
|||
}
|
||||
}
|
||||
GroupMemberStatus.MemAccepted -> {
|
||||
if (groupInfo.membership.memberCurrentOrPending) {
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu)
|
||||
}
|
||||
if (groupInfo.canDelete) {
|
||||
|
@ -326,7 +327,7 @@ fun GroupMenuItems(
|
|||
}
|
||||
}
|
||||
ClearChatAction(chat, showMenu)
|
||||
if (groupInfo.membership.memberCurrentOrPending) {
|
||||
if (groupInfo.membership.memberCurrent) {
|
||||
LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu)
|
||||
}
|
||||
if (groupInfo.canDelete) {
|
||||
|
@ -614,8 +615,7 @@ fun markChatRead(c: Chat) {
|
|||
chatModel.controller.apiChatRead(
|
||||
chat.remoteHostId,
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
chat.chatInfo.groupChatScope()
|
||||
chat.chatInfo.apiId
|
||||
)
|
||||
chat = chatModel.getChat(chat.id) ?: return@withApi
|
||||
}
|
||||
|
@ -651,7 +651,7 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
|
|||
if (success) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
|
||||
chatModel.chatsContext.updateChatTagReadInPrimaryContext(chat, wasUnread)
|
||||
chatModel.chatsContext.updateChatTagReadNoContentTag(chat, wasUnread)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -886,7 +886,7 @@ fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: Ch
|
|||
ChatInfo.Direct(contact.copy(chatSettings = chatSettings))
|
||||
}
|
||||
is ChatInfo.Group -> with(chatInfo) {
|
||||
ChatInfo.Group(groupInfo.copy(chatSettings = chatSettings), groupChatScope = null)
|
||||
ChatInfo.Group(groupInfo.copy(chatSettings = chatSettings))
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
@ -914,7 +914,7 @@ fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: Ch
|
|||
val updatedChat = chatModel.getChat(chatInfo.id)
|
||||
if (updatedChat != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateChatTagReadInPrimaryContext(updatedChat, wasUnread)
|
||||
chatModel.chatsContext.updateChatTagReadNoContentTag(updatedChat, wasUnread)
|
||||
}
|
||||
}
|
||||
val current = currentState?.value
|
||||
|
|
|
@ -241,8 +241,6 @@ fun ChatPreviewView(
|
|||
GroupMemberStatus.MemRejected -> Text(stringResource(MR.strings.group_preview_rejected))
|
||||
GroupMemberStatus.MemInvited -> Text(groupInvitationPreviewText(currentUserProfileDisplayName, cInfo.groupInfo))
|
||||
GroupMemberStatus.MemAccepted -> Text(stringResource(MR.strings.group_connection_pending), color = MaterialTheme.colors.secondary)
|
||||
GroupMemberStatus.MemPendingReview, GroupMemberStatus.MemPendingApproval ->
|
||||
Text(stringResource(MR.strings.reviewed_by_admins), color = MaterialTheme.colors.secondary)
|
||||
else -> {}
|
||||
}
|
||||
else -> {}
|
||||
|
@ -365,11 +363,7 @@ fun ChatPreviewView(
|
|||
if (progressByTimeout) {
|
||||
progressView()
|
||||
} else if (chat.chatStats.reportsCount > 0) {
|
||||
FlagIcon(color = MaterialTheme.colors.error)
|
||||
} else if (chat.supportUnreadCount > 0) {
|
||||
FlagIcon(color = MaterialTheme.colors.primary)
|
||||
} else if (chat.chatInfo.groupInfo_?.membership?.memberPending == true) {
|
||||
FlagIcon(color = MaterialTheme.colors.secondary)
|
||||
GroupReportsIcon()
|
||||
} else {
|
||||
IncognitoIcon(chat.chatInfo.incognito)
|
||||
}
|
||||
|
@ -471,10 +465,17 @@ fun ChatPreviewView(
|
|||
)
|
||||
}
|
||||
} else {
|
||||
UnreadBadge(
|
||||
text = if (n > 0) unreadCountStr(n) else "",
|
||||
backgroundColor = if (disabled || showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant,
|
||||
yOffset = 3.dp
|
||||
Text(
|
||||
if (n > 0) unreadCountStr(n) else "",
|
||||
color = Color.White,
|
||||
fontSize = 10.sp,
|
||||
style = TextStyle(textAlign = TextAlign.Center),
|
||||
modifier = Modifier
|
||||
.offset(y = 3.sp.toDp())
|
||||
.background(if (disabled || showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant, shape = CircleShape)
|
||||
.badgeLayout()
|
||||
.padding(horizontal = 2.sp.toDp())
|
||||
.padding(vertical = 1.sp.toDp())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -547,11 +548,11 @@ fun IncognitoIcon(incognito: Boolean) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun FlagIcon(color: Color) {
|
||||
fun GroupReportsIcon() {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_flag),
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
tint = MaterialTheme.colors.error,
|
||||
modifier = Modifier
|
||||
.size(21.sp.toDp())
|
||||
.offset(x = 2.sp.toDp())
|
||||
|
@ -566,26 +567,6 @@ private fun groupInvitationPreviewText(currentUserProfileDisplayName: String?, g
|
|||
stringResource(MR.strings.group_preview_you_are_invited)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UnreadBadge(
|
||||
text: String,
|
||||
backgroundColor: Color,
|
||||
yOffset: Dp? = null
|
||||
) {
|
||||
Text(
|
||||
text,
|
||||
color = Color.White,
|
||||
fontSize = 10.sp,
|
||||
style = TextStyle(textAlign = TextAlign.Center),
|
||||
modifier = Modifier
|
||||
.offset(y = yOffset ?: 0.dp)
|
||||
.background(backgroundColor, shape = CircleShape)
|
||||
.badgeLayout()
|
||||
.padding(horizontal = 2.sp.toDp())
|
||||
.padding(vertical = 1.sp.toDp())
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun unreadCountStr(n: Int): String {
|
||||
return if (n < 1000) "$n" else "${n / 1000}" + stringResource(MR.strings.thousand_abbreviation)
|
||||
|
|
|
@ -634,7 +634,7 @@ fun HostDisconnectButton(onClick: (() -> Unit)?) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun BoxScope.userUnreadBadge(unreadCount: Int, userMuted: Boolean, hasPadding: Boolean) {
|
||||
fun BoxScope.unreadBadge(unreadCount: Int, userMuted: Boolean, hasPadding: Boolean) {
|
||||
Text(
|
||||
if (unreadCount > 0) unreadCountStr(unreadCount) else "",
|
||||
color = Color.White,
|
||||
|
|
|
@ -45,6 +45,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, c
|
|||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.updateGroup(rhId = rhId, groupInfo)
|
||||
chatModel.chatsContext.chatItems.clearAndNotify()
|
||||
chatModel.chatsContext.chatItemStatuses.clear()
|
||||
chatModel.chatId.value = groupInfo.id
|
||||
}
|
||||
setGroupMembers(rhId, groupInfo, chatModel)
|
||||
|
|
|
@ -21,6 +21,7 @@ import dev.icerock.moko.resources.compose.stringResource
|
|||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.BuildConfigCommon
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
|
@ -127,7 +128,9 @@ fun SettingsLayout(
|
|||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_support)) {
|
||||
ContributeItem(uriHandler)
|
||||
if (!BuildConfigCommon.ANDROID_BUNDLE) {
|
||||
ContributeItem(uriHandler)
|
||||
}
|
||||
RateAppItem(uriHandler)
|
||||
StarOnGithubItem(uriHandler)
|
||||
}
|
||||
|
|
|
@ -158,7 +158,6 @@
|
|||
<string name="error_loading_details">Error loading details</string>
|
||||
<string name="error_adding_members">Error adding member(s)</string>
|
||||
<string name="error_joining_group">Error joining group</string>
|
||||
<string name="error_accepting_member">Error accepting member</string>
|
||||
<string name="cannot_receive_file">Cannot receive file</string>
|
||||
<string name="sender_cancelled_file_transfer">Sender cancelled file transfer.</string>
|
||||
<string name="file_not_approved_title">Unknown servers!</string>
|
||||
|
@ -465,10 +464,6 @@
|
|||
<string name="group_reports_active_one">1 report</string>
|
||||
<string name="group_reports_active">%d reports</string>
|
||||
<string name="group_reports_member_reports">Member reports</string>
|
||||
<string name="group_new_support_messages">%d messages</string>
|
||||
<string name="group_new_support_chats">%d chats with members</string>
|
||||
<string name="group_new_support_chat_one">1 chat with a member</string>
|
||||
<string name="group_new_support_chats_short">%d chat(s)</string>
|
||||
|
||||
<!-- ShareListView.kt -->
|
||||
<string name="share_message">Share message…</string>
|
||||
|
@ -492,9 +487,6 @@
|
|||
<string name="image_decoding_exception_desc">The image cannot be decoded. Please, try a different image or contact developers.</string>
|
||||
<string name="video_decoding_exception_desc">The video cannot be decoded. Please, try a different video or contact developers.</string>
|
||||
<string name="you_are_observer">you are observer</string>
|
||||
<string name="reviewed_by_admins">reviewed by admins</string>
|
||||
<string name="observer_cant_send_message_title">You can\'t send messages!</string>
|
||||
<string name="observer_cant_send_message_desc">Please contact group admin.</string>
|
||||
<string name="files_and_media_prohibited">Files and media prohibited!</string>
|
||||
<string name="only_owners_can_enable_files_and_media">Only group owners can enable files and media.</string>
|
||||
<string name="compose_send_direct_message_to_connect">Send direct message to connect</string>
|
||||
|
@ -513,8 +505,19 @@
|
|||
<string name="report_compose_reason_header_community">Report violation: only group moderators will see it.</string>
|
||||
<string name="report_compose_reason_header_illegal">Report content: only group moderators will see it.</string>
|
||||
<string name="report_compose_reason_header_other">Report other: only group moderators will see it.</string>
|
||||
<string name="report_sent_alert_title">Report sent to moderators</string>
|
||||
<string name="report_sent_alert_msg_view_in_support_chat">You can view your reports in Chat with admins.</string>
|
||||
|
||||
<string name="cant_send_message_alert_title">You can\'t send messages!</string>
|
||||
<string name="cant_send_message_contact_not_ready">contact not ready</string>
|
||||
<string name="cant_send_message_contact_deleted">contact deleted</string>
|
||||
<string name="cant_send_message_contact_not_synchronized">not synchronized</string>
|
||||
<string name="cant_send_message_contact_disabled">contact disabled</string>
|
||||
<string name="observer_cant_send_message_title">you are observer</string>
|
||||
<string name="observer_cant_send_message_desc">Please contact group admin.</string>
|
||||
<string name="cant_send_message_rejected">request to join rejected</string>
|
||||
<string name="cant_send_message_group_deleted">group is deleted</string>
|
||||
<string name="cant_send_message_mem_removed">removed from group</string>
|
||||
<string name="cant_send_message_you_left">you left</string>
|
||||
<string name="cant_send_message_generic">can\'t send messages</string>
|
||||
|
||||
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
|
||||
<string name="image_descr">Image</string>
|
||||
|
@ -1060,7 +1063,6 @@
|
|||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</string>
|
||||
<string name="edit_image">Edit image</string>
|
||||
<string name="delete_image">Delete image</string>
|
||||
<string name="save_admission_question">Save admission settings?</string>
|
||||
<string name="save_preferences_question">Save preferences?</string>
|
||||
<string name="save_and_notify_contact">Save and notify contact</string>
|
||||
<string name="save_and_notify_contacts">Save and notify contacts</string>
|
||||
|
@ -1580,8 +1582,6 @@
|
|||
<!-- Group event chat items -->
|
||||
<string name="rcv_group_event_member_added">invited %1$s</string>
|
||||
<string name="rcv_group_event_member_connected">connected</string>
|
||||
<string name="rcv_group_event_member_accepted">accepted %1$s</string>
|
||||
<string name="rcv_group_event_user_accepted">accepted you</string>
|
||||
<string name="rcv_group_event_member_left">left</string>
|
||||
<string name="rcv_group_event_changed_member_role">changed role of %s to %s</string>
|
||||
<string name="rcv_group_event_member_blocked">blocked %s</string>
|
||||
|
@ -1593,7 +1593,6 @@
|
|||
<string name="rcv_group_event_updated_group_profile">updated group profile</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">invited via your group link</string>
|
||||
<string name="rcv_group_event_member_created_contact">connected directly</string>
|
||||
<string name="rcv_group_event_new_member_pending_review">New member wants to join the group.</string>
|
||||
<string name="snd_group_event_changed_member_role">you changed role of %s to %s</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">you changed role for yourself to %s</string>
|
||||
<string name="snd_group_event_member_blocked">you blocked %s</string>
|
||||
|
@ -1601,8 +1600,6 @@
|
|||
<string name="snd_group_event_member_deleted">you removed %1$s</string>
|
||||
<string name="snd_group_event_user_left">you left</string>
|
||||
<string name="snd_group_event_group_profile_updated">group profile updated</string>
|
||||
<string name="snd_group_event_member_accepted">you accepted this member</string>
|
||||
<string name="snd_group_event_user_pending_review">Please wait for group moderators to review your request to join the group.</string>
|
||||
|
||||
<string name="rcv_group_event_1_member_connected">%s connected</string>
|
||||
<string name="rcv_group_event_2_members_connected">%s and %s connected</string>
|
||||
|
@ -1662,8 +1659,6 @@
|
|||
<string name="group_member_status_invited">invited</string>
|
||||
<string name="group_member_status_pending_approval">pending approval</string>
|
||||
<string name="group_member_status_pending_approval_short">pending</string>
|
||||
<string name="group_member_status_pending_review">pending review</string>
|
||||
<string name="group_member_status_pending_review_short">review</string>
|
||||
<string name="group_member_status_introduced">connecting (introduced)</string>
|
||||
<string name="group_member_status_intro_invitation">connecting (introduction invitation)</string>
|
||||
<string name="group_member_status_accepted">connecting (accepted)</string>
|
||||
|
@ -1734,7 +1729,6 @@
|
|||
<string name="send_receipts_disabled_alert_title">Receipts are disabled</string>
|
||||
<string name="send_receipts_disabled_alert_msg">This group has over %1$d members, delivery receipts are not sent.</string>
|
||||
<string name="action_button_add_members">Invite</string>
|
||||
<string name="button_support_chat">Chat with admins</string>
|
||||
|
||||
<!-- Chat / Chat item info -->
|
||||
<string name="section_title_for_console">FOR CONSOLE</string>
|
||||
|
@ -1771,7 +1765,7 @@
|
|||
<string name="button_remove_member_question">Remove member?</string>
|
||||
<string name="button_remove_members_question">Remove members?</string>
|
||||
<string name="button_remove_member">Remove member</string>
|
||||
<string name="button_support_chat_member">Chat with member</string>
|
||||
|
||||
<string name="button_send_direct_message">Send direct message</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">Member will be removed from group - this cannot be undone!</string>
|
||||
<string name="members_will_be_removed_from_group_cannot_be_undone">Members will be removed from group - this cannot be undone!</string>
|
||||
|
@ -2048,7 +2042,6 @@
|
|||
<string name="contact_preferences">Contact preferences</string>
|
||||
<string name="group_preferences">Group preferences</string>
|
||||
<string name="set_group_preferences">Set group preferences</string>
|
||||
<string name="set_member_admission">Set member admission</string>
|
||||
<string name="your_preferences">Your preferences</string>
|
||||
<string name="timed_messages">Disappearing messages</string>
|
||||
<string name="direct_messages">Direct messages</string>
|
||||
|
@ -2166,26 +2159,6 @@
|
|||
<string name="feature_roles_owners">owners</string>
|
||||
<string name="feature_enabled_for">Enabled for</string>
|
||||
|
||||
<!-- MemberAdmission.kt -->
|
||||
<string name="member_admission">Member admission</string>
|
||||
<string name="admission_stage_review">Review members</string>
|
||||
<string name="admission_stage_review_descr">Review members before admitting ("knocking").</string>
|
||||
<string name="member_criteria_off">off</string>
|
||||
<string name="member_criteria_all">all</string>
|
||||
|
||||
<!-- MemberSupportView.kt -->
|
||||
<string name="member_support">Chats with members</string>
|
||||
<string name="no_support_chats">No chats with members</string>
|
||||
|
||||
<!-- MemberSupportChatView.kt -->
|
||||
<string name="support_chat">Chat with admins</string>
|
||||
<string name="remove_pending_member_button">Remove</string>
|
||||
<string name="accept_pending_member_button">Accept</string>
|
||||
<string name="accept_pending_member_alert_title">Accept member</string>
|
||||
<string name="accept_pending_member_alert_question">Member will join the group, accept member?</string>
|
||||
<string name="accept_pending_member_alert_confirmation_as_member">Accept as member</string>
|
||||
<string name="accept_pending_member_alert_confirmation_as_observer">Accept as observer</string>
|
||||
|
||||
<!-- WhatsNewView.kt -->
|
||||
<string name="whats_new">What\'s new</string>
|
||||
<string name="new_in_version">New in %s</string>
|
||||
|
|
|
@ -44,11 +44,10 @@ import kotlin.text.substring
|
|||
actual fun PlatformTextField(
|
||||
composeState: MutableState<ComposeState>,
|
||||
sendMsgEnabled: Boolean,
|
||||
disabledText: String?,
|
||||
sendMsgButtonDisabled: Boolean,
|
||||
textStyle: MutableState<TextStyle>,
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
userIsPending: Boolean,
|
||||
placeholder: String,
|
||||
showVoiceButton: Boolean,
|
||||
onMessageChange: (ComposeMessage) -> Unit,
|
||||
|
@ -204,18 +203,16 @@ actual fun PlatformTextField(
|
|||
)
|
||||
showDeleteTextButton.value = cs.message.text.split("\n").size >= 4 && !cs.inProgress
|
||||
if (composeState.value.preview is ComposePreview.VoicePreview) {
|
||||
ComposeOverlay(MR.strings.voice_message_send_text, textStyle, padding)
|
||||
} else if (userIsPending) {
|
||||
ComposeOverlay(MR.strings.reviewed_by_admins, textStyle, padding)
|
||||
} else if (userIsObserver) {
|
||||
ComposeOverlay(MR.strings.you_are_observer, textStyle, padding)
|
||||
ComposeOverlay(generalGetString(MR.strings.voice_message_send_text), textStyle, padding)
|
||||
} else if (disabledText != null) {
|
||||
ComposeOverlay(disabledText, textStyle, padding)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ComposeOverlay(textId: StringResource, textStyle: MutableState<TextStyle>, padding: PaddingValues) {
|
||||
private fun ComposeOverlay(text: String, textStyle: MutableState<TextStyle>, padding: PaddingValues) {
|
||||
Text(
|
||||
generalGetString(textId),
|
||||
text,
|
||||
Modifier.padding(padding),
|
||||
color = MaterialTheme.colors.secondary,
|
||||
style = textStyle.value.copy(fontStyle = FontStyle.Italic)
|
||||
|
|
|
@ -63,7 +63,7 @@ actual fun UserPickerUsersSection(
|
|||
ProfileImage(size = 55.dp, image = user.profile.image, color = iconColor)
|
||||
|
||||
if (u.unreadCount > 0 && !user.activeUser) {
|
||||
userUnreadBadge(u.unreadCount, user.showNtfs, true)
|
||||
unreadBadge(u.unreadCount, user.showNtfs, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,11 +24,13 @@ android.nonTransitiveRClass=true
|
|||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=6.4-beta.0
|
||||
android.version_code=290
|
||||
android.version_name=6.3.6
|
||||
android.version_code=295
|
||||
|
||||
desktop.version_name=6.4-beta.0
|
||||
desktop.version_code=102
|
||||
android.bundle=false
|
||||
|
||||
desktop.version_name=6.3.6
|
||||
desktop.version_code=106
|
||||
|
||||
kotlin.version=1.9.23
|
||||
gradle.plugin.version=8.2.0
|
||||
|
|
|
@ -79,7 +79,7 @@ crDirectoryEvent_ = \case
|
|||
CEvtJoinedGroupMember {groupInfo, member = m}
|
||||
| pending m -> Just $ DEPendingMember groupInfo m
|
||||
| otherwise -> Nothing
|
||||
CEvtNewChatItems {chatItems = AChatItem _ _ (GroupChat g _scopeInfo) ci : _} -> case ci of
|
||||
CEvtNewChatItems {chatItems = AChatItem _ _ (GroupChat g) ci : _} -> case ci of
|
||||
ChatItem {chatDir = CIGroupRcv m, content = CIRcvMsgContent (MCText t)} | pending m -> Just $ DEPendingMemberMsg g m (chatItemId' ci) t
|
||||
_ -> Nothing
|
||||
CEvtMemberRole {groupInfo, member, toRole}
|
||||
|
|
|
@ -167,7 +167,7 @@ acceptMemberHook
|
|||
when (useMemberFilter img $ rejectNames a) checkName
|
||||
pure $
|
||||
if
|
||||
| useMemberFilter img (passCaptcha a) -> (GAPendingApproval, GRMember)
|
||||
| useMemberFilter img (passCaptcha a) -> (GAPending, GRMember)
|
||||
| useMemberFilter img (makeObserver a) -> (GAAccepted, GRObserver)
|
||||
| otherwise -> (GAAccepted, memberRole)
|
||||
where
|
||||
|
@ -494,7 +494,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
|||
[] -> textMsg
|
||||
"" : _ -> textMsg
|
||||
img : _ -> MCImage "" $ ImageData img
|
||||
sendCaptcha mc = sendComposedMessages_ cc (SRGroup groupId $ Just $ GCSMemberSupport (Just gmId)) [(quotedId, MCText noticeText), (Nothing, mc)]
|
||||
sendCaptcha mc = sendComposedMessages_ cc (SRGroup groupId $ Just gmId) [(quotedId, MCText noticeText), (Nothing, mc)]
|
||||
gmId = groupMemberId' m
|
||||
|
||||
approvePendingMember :: DirectoryMemberAcceptance -> GroupInfo -> GroupMember -> IO ()
|
||||
|
@ -503,11 +503,9 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
|||
let role = if useMemberFilter image (makeObserver a) then GRObserver else maybe GRMember (\GroupLinkInfo {memberRole} -> memberRole) gli_
|
||||
gmId = groupMemberId' m
|
||||
sendChatCmd cc (APIAcceptMember groupId gmId role) >>= \case
|
||||
Right CRMemberAccepted {member} -> do
|
||||
Right CRJoinedGroupMember {} -> do
|
||||
atomically $ TM.delete gmId $ pendingCaptchas env
|
||||
if memberStatus member == GSMemPendingReview
|
||||
then logInfo $ "Member " <> viewName displayName <> " accepted and pending review, group " <> tshow groupId <> ":" <> viewGroupName g
|
||||
else logInfo $ "Member " <> viewName displayName <> " accepted, group " <> tshow groupId <> ":" <> viewGroupName g
|
||||
logInfo $ "Member " <> viewName displayName <> " accepted, group " <> tshow groupId <> ":" <> viewGroupName g
|
||||
r -> logError $ "unexpected accept member response: " <> tshow r
|
||||
|
||||
dePendingMemberMsg :: GroupInfo -> GroupMember -> ChatItemId -> Text -> IO ()
|
||||
|
@ -518,7 +516,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
|||
Just PendingCaptcha {captchaText, sentAt, attempts}
|
||||
| ts `diffUTCTime` sentAt > captchaTTL -> sendMemberCaptcha g m (Just ciId) captchaExpired $ attempts - 1
|
||||
| matchCaptchaStr captchaText msgText -> do
|
||||
sendComposedMessages_ cc (SRGroup groupId $ Just $ GCSMemberSupport (Just $ groupMemberId' m)) [(Just ciId, MCText $ "Correct, you joined the group " <> n)]
|
||||
sendComposedMessages_ cc (SRGroup groupId $ Just $ groupMemberId' m) [(Just ciId, MCText $ "Correct, you joined the group " <> n)]
|
||||
approvePendingMember a g m
|
||||
| attempts >= maxCaptchaAttempts -> rejectPendingMember tooManyAttempts
|
||||
| otherwise -> sendMemberCaptcha g m (Just ciId) (wrongCaptcha attempts) attempts
|
||||
|
@ -528,7 +526,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
|||
a = groupMemberAcceptance g
|
||||
rejectPendingMember rjctNotice = do
|
||||
let gmId = groupMemberId' m
|
||||
sendComposedMessages cc (SRGroup groupId $ Just $ GCSMemberSupport (Just gmId)) [MCText rjctNotice]
|
||||
sendComposedMessages cc (SRGroup groupId $ Just gmId) [MCText rjctNotice]
|
||||
sendChatCmd cc (APIRemoveMembers groupId [gmId] False) >>= \case
|
||||
Right (CRUserDeletedMembers _ _ (_ : _) _) -> do
|
||||
atomically $ TM.delete gmId $ pendingCaptchas env
|
||||
|
|
|
@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
|||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: f44ea0a6d8eec8abf4af177ebeb91629f7d89165
|
||||
tag: 3d62a383d5dcae6529d6d866233857182bcb4d47
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
# Group knocking
|
||||
|
||||
## Problem
|
||||
|
||||
In v6.3 release we added an option to "manually" approve members before introducing to group, based on decision made via `acceptMember` chat hook. Currently it's not supported in UI clients, and only used for directory service bot captcha challenge.
|
||||
|
||||
The goal of next improvement is to let:
|
||||
- admins screen members before admitting to group, while not limiting communication with joining member to a single admin (and also removing the need for admin to be a highly available host of the group link);
|
||||
- and group owners set up other arbitrary automated challenges or entry rules, while still being able to advertise groups in directory service.
|
||||
|
||||
## Solution
|
||||
|
||||
Group link host (further host), knowing group requires admin approval, would initially only introduce member pending approval to admins. Admins can connect with member for screening, meanwhile host would be forwarding messages as usual between connecting members. As a result of screening, pending member can either be removed, or approved by admins.
|
||||
|
||||
Upon acceptance, for further member connections to not depend on availability of admins, host should not only forward acceptance message, but also introduce remaining members to now accepted joining member. Respectively, admins' clients should not introduce members for approved members who are not their invitees.
|
||||
|
||||
For group owners to be able to set up alternative automated challenges, these are some possible alternatives:
|
||||
- We could add a new role `Approver`, so that instead of adding all \[possibly human\] admins, host would initially introduce only approvers.
|
||||
- It could be an orthogonal to role member setting (would require protocol extension).
|
||||
- List of "approver" member IDs could be communicated to host client.
|
||||
|
||||
### Implementation details draft
|
||||
|
||||
Host needs to have knowledge whether to automatically accept, or only introduce admins/approvers.
|
||||
|
||||
```sql
|
||||
ALTER TABLE group_profiles ADD COLUMN approval TEXT; -- comma separated member IDs; null - automatic introduction
|
||||
|
||||
-- or
|
||||
|
||||
ALTER TABLE group_profiles ADD COLUMN approval INTEGER; -- if based on `Approver` role
|
||||
```
|
||||
|
||||
Alternatively, a different extension of protocol could be done in order to communicate group approval rule from owner to host outside of group profile (special messages).
|
||||
|
||||
Admins/approvers need to have separate conversation per pending member, requires adding scope to chat items.
|
||||
|
||||
Host to have specific processing of forwarded `XGrpLinkAcpt` - continue introduction of remaining members.
|
Binary file not shown.
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
BIN
images/whonix-logo.jpg
Normal file
BIN
images/whonix-logo.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 8 KiB |
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."f44ea0a6d8eec8abf4af177ebeb91629f7d89165" = "1biq1kq33v7hnacbhllry9n5c6dmh9dyqnz8hc5abgsv1z38qb1a";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."3d62a383d5dcae6529d6d866233857182bcb4d47" = "133xm8jkim7agd6drwm3lbx1z7v8nf4l3asrm46ag3n2q201yfxc";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||
|
|
|
@ -5,7 +5,7 @@ cabal-version: 1.12
|
|||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 6.4.0.1
|
||||
version: 6.3.6.0
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
|
@ -105,7 +105,6 @@ library
|
|||
Simplex.Chat.Store.Postgres.Migrations.M20241220_initial
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20250402_short_links
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20250512_member_admission
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20250513_group_scope
|
||||
else
|
||||
exposed-modules:
|
||||
Simplex.Chat.Archive
|
||||
|
@ -237,7 +236,6 @@ library
|
|||
Simplex.Chat.Store.SQLite.Migrations.M20250130_indexes
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20250513_group_scope
|
||||
other-modules:
|
||||
Paths_simplex_chat
|
||||
hs-source-dirs:
|
||||
|
|
|
@ -95,7 +95,7 @@ deleteMessage cc ct chatItemId = do
|
|||
r -> putStrLn $ "unexpected delete message response: " <> show r
|
||||
|
||||
contactRef :: Contact -> ChatRef
|
||||
contactRef ct = ChatRef CTDirect (contactId' ct) Nothing
|
||||
contactRef = ChatRef CTDirect . contactId'
|
||||
|
||||
printLog :: ChatController -> ChatLogLevel -> String -> IO ()
|
||||
printLog cc level s
|
||||
|
|
|
@ -365,8 +365,6 @@ data ChatCommand
|
|||
| APIRemoveMembers {groupId :: GroupId, groupMemberIds :: Set GroupMemberId, withMessages :: Bool}
|
||||
| APILeaveGroup GroupId
|
||||
| APIListMembers GroupId
|
||||
-- | APIDeleteGroupConversations GroupId (NonEmpty GroupConversationId)
|
||||
-- | APIArchiveGroupConversations GroupId (NonEmpty GroupConversationId)
|
||||
| APIUpdateGroupProfile GroupId GroupProfile
|
||||
| APICreateGroupLink GroupId GroupMemberRole CreateShortLink
|
||||
| APIGroupLinkMemberRole GroupId GroupMemberRole
|
||||
|
@ -490,7 +488,6 @@ data ChatCommand
|
|||
| DeleteGroup GroupName
|
||||
| ClearGroup GroupName
|
||||
| ListMembers GroupName
|
||||
| ListMemberSupportChats GroupName
|
||||
| APIListGroups UserId (Maybe ContactId) (Maybe String)
|
||||
| ListGroups (Maybe ContactName) (Maybe String)
|
||||
| UpdateGroupNames GroupName GroupProfile
|
||||
|
@ -526,7 +523,6 @@ data ChatCommand
|
|||
| SetContactFeature AChatFeature ContactName (Maybe FeatureAllowed)
|
||||
| SetGroupFeature AGroupFeatureNoRole GroupName GroupFeatureEnabled
|
||||
| SetGroupFeatureRole AGroupFeatureRole GroupName GroupFeatureEnabled (Maybe GroupMemberRole)
|
||||
| SetGroupMemberAdmissionReview GroupName (Maybe MemberCriteria)
|
||||
| SetUserTimedMessages Bool -- UserId (not used in UI)
|
||||
| SetContactTimedMessages ContactName (Maybe TimedMessagesEnabled)
|
||||
| SetGroupTimedMessages GroupName (Maybe Int)
|
||||
|
@ -630,9 +626,9 @@ data ChatResponse
|
|||
| CRGroupMemberInfo {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats}
|
||||
| CRQueueInfo {user :: User, rcvMsgInfo :: Maybe RcvMsgInfo, queueInfo :: ServerQueueInfo}
|
||||
| CRContactSwitchStarted {user :: User, contact :: Contact, connectionStats :: ConnectionStats}
|
||||
| CEvtGroupMemberSwitchStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats}
|
||||
| CRGroupMemberSwitchStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats}
|
||||
| CRContactSwitchAborted {user :: User, contact :: Contact, connectionStats :: ConnectionStats}
|
||||
| CEvtGroupMemberSwitchAborted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats}
|
||||
| CRGroupMemberSwitchAborted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats}
|
||||
| CRContactRatchetSyncStarted {user :: User, contact :: Contact, connectionStats :: ConnectionStats}
|
||||
| CRGroupMemberRatchetSyncStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats}
|
||||
| CRContactCode {user :: User, contact :: Contact, connectionCode :: Text}
|
||||
|
@ -652,9 +648,6 @@ data ChatResponse
|
|||
| CRWelcome {user :: User}
|
||||
| CRGroupCreated {user :: User, groupInfo :: GroupInfo}
|
||||
| CRGroupMembers {user :: User, group :: Group}
|
||||
| CRMemberSupportChats {user :: User, groupInfo :: GroupInfo, members :: [GroupMember]}
|
||||
-- | CRGroupConversationsArchived {user :: User, groupInfo :: GroupInfo, archivedGroupConversations :: [GroupConversation]}
|
||||
-- | CRGroupConversationsDeleted {user :: User, groupInfo :: GroupInfo, deletedGroupConversations :: [GroupConversation]}
|
||||
| CRContactsList {user :: User, contacts :: [Contact]}
|
||||
| CRUserContactLink {user :: User, contactLink :: UserContactLink}
|
||||
| CRUserContactLinkUpdated {user :: User, contactLink :: UserContactLink}
|
||||
|
@ -676,7 +669,6 @@ data ChatResponse
|
|||
| CRSentConfirmation {user :: User, connection :: PendingContactConnection}
|
||||
| CRSentInvitation {user :: User, connection :: PendingContactConnection, customUserProfile :: Maybe Profile}
|
||||
| CRSentInvitationToContact {user :: User, contact :: Contact, customUserProfile :: Maybe Profile}
|
||||
| CRItemsReadForChat {user :: User, chatInfo :: AChatInfo}
|
||||
| CRContactDeleted {user :: User, contact :: Contact}
|
||||
| CRChatCleared {user :: User, chatInfo :: AChatInfo}
|
||||
| CRUserContactLinkCreated {user :: User, connLinkContact :: CreatedLinkContact}
|
||||
|
@ -702,7 +694,6 @@ data ChatResponse
|
|||
| CRContactPrefsUpdated {user :: User, fromContact :: Contact, toContact :: Contact}
|
||||
| CRNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]}
|
||||
| CRJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRMemberAccepted {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
|
||||
| CRMembersRoleUser {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], toRole :: GroupMemberRole}
|
||||
| CRMembersBlockedForAllUser {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], blocked :: Bool}
|
||||
| CRGroupUpdated {user :: User, fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember}
|
||||
|
@ -811,7 +802,6 @@ data ChatEvent
|
|||
| CEvtUserJoinedGroup {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember}
|
||||
| CEvtJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} -- there is the same command response
|
||||
| CEvtJoinedGroupMemberConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember}
|
||||
| CEvtMemberAcceptedByOther {user :: User, groupInfo :: GroupInfo, acceptingMember :: GroupMember, member :: GroupMember}
|
||||
| CEvtMemberRole {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, fromRole :: GroupMemberRole, toRole :: GroupMemberRole}
|
||||
| CEvtMemberBlockedForAll {user :: User, groupInfo :: GroupInfo, byMember :: GroupMember, member :: GroupMember, blocked :: Bool}
|
||||
| CEvtConnectedToGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember, memberContact :: Maybe Contact}
|
||||
|
@ -909,15 +899,16 @@ logEventToFile = \case
|
|||
_ -> False
|
||||
_ -> False
|
||||
|
||||
-- (Maybe GroupMemberId) can later be changed to GroupSndScope = GSSAll | GSSAdmins | GSSMember GroupMemberId
|
||||
data SendRef
|
||||
= SRDirect ContactId
|
||||
| SRGroup GroupId (Maybe GroupChatScope)
|
||||
| SRGroup GroupId (Maybe GroupMemberId)
|
||||
deriving (Eq, Show)
|
||||
|
||||
sendToChatRef :: SendRef -> ChatRef
|
||||
sendToChatRef = \case
|
||||
SRDirect cId -> ChatRef CTDirect cId Nothing
|
||||
SRGroup gId scope -> ChatRef CTGroup gId scope
|
||||
SRDirect cId -> ChatRef CTDirect cId
|
||||
SRGroup gId _ -> ChatRef CTGroup gId
|
||||
|
||||
data ChatPagination
|
||||
= CPLast Int
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -190,13 +190,13 @@ toggleNtf m ntfOn =
|
|||
forM_ (memberConnId m) $ \connId ->
|
||||
withAgent (\a -> toggleConnectionNtfs a connId ntfOn) `catchChatError` eToView
|
||||
|
||||
prepareGroupMsg :: DB.Connection -> User -> GroupInfo -> Maybe MsgScope -> MsgContent -> Map MemberName MsgMention -> Maybe ChatItemId -> Maybe CIForwardedFrom -> Maybe FileInvitation -> Maybe CITimed -> Bool -> ExceptT StoreError IO (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup))
|
||||
prepareGroupMsg db user g@GroupInfo {membership} msgScope mc mentions quotedItemId_ itemForwarded fInv_ timed_ live = case (quotedItemId_, itemForwarded) of
|
||||
prepareGroupMsg :: DB.Connection -> User -> GroupInfo -> MsgContent -> Map MemberName MsgMention -> Maybe ChatItemId -> Maybe CIForwardedFrom -> Maybe FileInvitation -> Maybe CITimed -> Bool -> ExceptT StoreError IO (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup))
|
||||
prepareGroupMsg db user g@GroupInfo {membership} mc mentions quotedItemId_ itemForwarded fInv_ timed_ live = case (quotedItemId_, itemForwarded) of
|
||||
(Nothing, Nothing) ->
|
||||
let mc' = MCSimple $ ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live) msgScope
|
||||
let mc' = MCSimple $ ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live)
|
||||
in pure (XMsgNew mc', Nothing)
|
||||
(Nothing, Just _) ->
|
||||
let mc' = MCForward $ ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live) msgScope
|
||||
let mc' = MCForward $ ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live)
|
||||
in pure (XMsgNew mc', Nothing)
|
||||
(Just quotedItemId, Nothing) -> do
|
||||
CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, mentions = quoteMentions, file} <-
|
||||
|
@ -206,7 +206,7 @@ prepareGroupMsg db user g@GroupInfo {membership} msgScope mc mentions quotedItem
|
|||
qmc = quoteContent mc origQmc file
|
||||
(qmc', ft', _) = updatedMentionNames qmc formattedText quoteMentions
|
||||
quotedItem = CIQuote {chatDir = qd, itemId = Just quotedItemId, sharedMsgId = itemSharedMsgId, sentAt = itemTs, content = qmc', formattedText = ft'}
|
||||
mc' = MCQuote QuotedMsg {msgRef, content = qmc'} (ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live) msgScope)
|
||||
mc' = MCQuote QuotedMsg {msgRef, content = qmc'} (ExtMsgContent mc mentions fInv_ (ttl' <$> timed_) (justTrue live))
|
||||
pure (XMsgNew mc', Just quotedItem)
|
||||
(Just _, Just _) -> throwError SEInvalidQuote
|
||||
where
|
||||
|
@ -453,8 +453,8 @@ deleteDirectCIs user ct items = do
|
|||
deleteDirectChatItem db user ct ci
|
||||
pure $ contactDeletion md ct ci Nothing
|
||||
|
||||
deleteGroupCIs :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> [CChatItem 'CTGroup] -> Maybe GroupMember -> UTCTime -> CM [ChatItemDeletion]
|
||||
deleteGroupCIs user gInfo chatScopeInfo items byGroupMember_ deletedTs = do
|
||||
deleteGroupCIs :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Maybe GroupMember -> UTCTime -> CM [ChatItemDeletion]
|
||||
deleteGroupCIs user gInfo items byGroupMember_ deletedTs = do
|
||||
let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items
|
||||
deleteCIFiles user ciFilesInfo
|
||||
(errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items)
|
||||
|
@ -466,7 +466,7 @@ deleteGroupCIs user gInfo chatScopeInfo items byGroupMember_ deletedTs = do
|
|||
ci' <- case byGroupMember_ of
|
||||
Just m -> Just <$> updateGroupChatItemModerated db user gInfo ci m deletedTs
|
||||
Nothing -> Nothing <$ deleteGroupChatItem db user gInfo ci
|
||||
pure $ groupDeletion md gInfo chatScopeInfo ci ci'
|
||||
pure $ groupDeletion md gInfo ci ci'
|
||||
|
||||
deleteGroupMemberCIs :: MsgDirectionI d => User -> GroupInfo -> GroupMember -> GroupMember -> SMsgDirection d -> CM ()
|
||||
deleteGroupMemberCIs user gInfo member byGroupMember msgDir = do
|
||||
|
@ -517,8 +517,8 @@ markDirectCIsDeleted user ct items deletedTs = do
|
|||
ci' <- markDirectChatItemDeleted db user ct ci deletedTs
|
||||
pure $ contactDeletion md ct ci (Just ci')
|
||||
|
||||
markGroupCIsDeleted :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> [CChatItem 'CTGroup] -> Maybe GroupMember -> UTCTime -> CM [ChatItemDeletion]
|
||||
markGroupCIsDeleted user gInfo chatScopeInfo items byGroupMember_ deletedTs = do
|
||||
markGroupCIsDeleted :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Maybe GroupMember -> UTCTime -> CM [ChatItemDeletion]
|
||||
markGroupCIsDeleted user gInfo items byGroupMember_ deletedTs = do
|
||||
let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items
|
||||
cancelFilesInProgress user ciFilesInfo
|
||||
(errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items)
|
||||
|
@ -528,7 +528,7 @@ markGroupCIsDeleted user gInfo chatScopeInfo items byGroupMember_ deletedTs = do
|
|||
where
|
||||
markDeleted db (CChatItem md ci) = do
|
||||
ci' <- markGroupChatItemDeleted db user gInfo ci byGroupMember_ deletedTs
|
||||
pure $ groupDeletion md gInfo chatScopeInfo ci (Just ci')
|
||||
pure $ groupDeletion md gInfo ci (Just ci')
|
||||
|
||||
markGroupMemberCIsDeleted :: User -> GroupInfo -> GroupMember -> GroupMember -> CM ()
|
||||
markGroupMemberCIsDeleted user gInfo member byGroupMember = do
|
||||
|
@ -548,10 +548,10 @@ markGroupMemberCIsDeleted_ db user gInfo member byGroupMember deletedTs = do
|
|||
markMemberCIsDeleted db user gInfo member byGroupMember deletedTs
|
||||
pure fs
|
||||
|
||||
groupDeletion :: MsgDirectionI d => SMsgDirection d -> GroupInfo -> Maybe GroupChatScopeInfo -> ChatItem 'CTGroup d -> Maybe (ChatItem 'CTGroup d) -> ChatItemDeletion
|
||||
groupDeletion md g chatScopeInfo ci ci' = ChatItemDeletion (gItem ci) (gItem <$> ci')
|
||||
groupDeletion :: MsgDirectionI d => SMsgDirection d -> GroupInfo -> ChatItem 'CTGroup d -> Maybe (ChatItem 'CTGroup d) -> ChatItemDeletion
|
||||
groupDeletion md g ci ci' = ChatItemDeletion (gItem ci) (gItem <$> ci')
|
||||
where
|
||||
gItem = AChatItem SCTGroup md (GroupChat g chatScopeInfo)
|
||||
gItem = AChatItem SCTGroup md (GroupChat g)
|
||||
|
||||
contactDeletion :: MsgDirectionI d => SMsgDirection d -> Contact -> ChatItem 'CTDirect d -> Maybe (ChatItem 'CTDirect d) -> ChatItemDeletion
|
||||
contactDeletion md ct ci ci' = ChatItemDeletion (ctItem ci) (ctItem <$> ci')
|
||||
|
@ -565,7 +565,7 @@ updateCallItemStatus user ct@Contact {contactId} Call {chatItemId} receivedStatu
|
|||
timed_ <- callTimed ct aciContent
|
||||
updateDirectChatItemView user ct chatItemId aciContent False False timed_ msgId_
|
||||
forM_ (timed_ >>= timedDeleteAt') $
|
||||
startProximateTimedItemThread user (ChatRef CTDirect contactId Nothing, chatItemId)
|
||||
startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId)
|
||||
|
||||
updateDirectChatItemView :: User -> Contact -> ChatItemId -> ACIContent -> Bool -> Bool -> Maybe CITimed -> Maybe MessageId -> CM ()
|
||||
updateDirectChatItemView user ct chatItemId (ACIContent msgDir ciContent) edited live timed_ msgId_ = do
|
||||
|
@ -668,10 +668,10 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
|
|||
_ -> do
|
||||
chatRef <- withStore $ \db -> getChatRefByFileId db user fileId
|
||||
case (chatRef, grpMemberId) of
|
||||
(ChatRef CTDirect contactId _, Nothing) -> do
|
||||
(ChatRef CTDirect contactId, Nothing) -> do
|
||||
ct <- withStore $ \db -> getContact db vr user contactId
|
||||
acceptFile CFCreateConnFileInvDirect $ \msg -> void $ sendDirectContactMessage user ct msg
|
||||
(ChatRef CTGroup groupId _, Just memId) -> do
|
||||
(ChatRef CTGroup groupId, Just memId) -> do
|
||||
GroupMember {activeConn} <- withStore $ \db -> getGroupMember db vr user groupId memId
|
||||
case activeConn of
|
||||
Just conn -> do
|
||||
|
@ -880,7 +880,7 @@ acceptGroupJoinRequestAsync
|
|||
gLinkMemRole
|
||||
incognitoProfile = do
|
||||
gVar <- asks random
|
||||
let initialStatus = acceptanceToStatus (memberAdmission groupProfile) gAccepted
|
||||
let initialStatus = acceptanceToStatus gAccepted
|
||||
(groupMemberId, memberId) <- withStore $ \db -> do
|
||||
liftIO $ deleteContactRequestRec db user ucr
|
||||
createJoiningMember db gVar user gInfo ucr gLinkMemRole initialStatus
|
||||
|
@ -965,7 +965,7 @@ acceptBusinessJoinRequestAsync
|
|||
let chatV = vr `peerConnChatVersion` cReqChatVRange
|
||||
connIds <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV
|
||||
withStore' $ \db -> createJoiningMemberConnection db user connIds chatV ucr groupMemberId subMode
|
||||
let cd = CDGroupSnd gInfo Nothing
|
||||
let cd = CDGroupSnd gInfo
|
||||
createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing
|
||||
createGroupFeatureItems user cd CISndGroupFeature gInfo
|
||||
pure gInfo
|
||||
|
@ -981,41 +981,13 @@ profileToSendOnAccept user ip = userProfileToSend user (getIncognitoProfile <$>
|
|||
NewIncognito p -> p
|
||||
ExistingIncognito lp -> fromLocalProfile lp
|
||||
|
||||
introduceToModerators :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM ()
|
||||
introduceToModerators vr user gInfo@GroupInfo {groupId} m@GroupMember {memberRole, memberId} = do
|
||||
forM_ (memberConn m) $ \mConn -> do
|
||||
let msg =
|
||||
if (maxVersion (memberChatVRange m) >= groupKnockingVersion)
|
||||
then XGrpLinkAcpt GAPendingReview memberRole memberId
|
||||
else XMsgNew $ MCSimple $ extMsgContent (MCText pendingReviewMessage) Nothing
|
||||
void $ sendDirectMemberMessage mConn msg groupId
|
||||
modMs <- withStore' $ \db -> getGroupModerators db vr user gInfo
|
||||
let rcpModMs = filter (\mem -> memberCurrent mem && maxVersion (memberChatVRange mem) >= groupKnockingVersion) modMs
|
||||
introduceMember vr user gInfo m rcpModMs (Just $ MSMember $ memberId' m)
|
||||
|
||||
introduceToAll :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM ()
|
||||
introduceToAll vr user gInfo m = do
|
||||
introduceToGroup :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM ()
|
||||
introduceToGroup _ _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active"
|
||||
introduceToGroup vr user gInfo@GroupInfo {groupId, membership} m@GroupMember {activeConn = Just conn} = do
|
||||
members <- withStore' $ \db -> getGroupMembers db vr user gInfo
|
||||
let recipients = filter memberCurrent members
|
||||
introduceMember vr user gInfo m recipients Nothing
|
||||
|
||||
introduceToRemaining :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM ()
|
||||
introduceToRemaining vr user gInfo m = do
|
||||
(members, introducedGMIds) <-
|
||||
withStore' $ \db -> (,) <$> getGroupMembers db vr user gInfo <*> getIntroducedGroupMemberIds db m
|
||||
let recipients = filter (introduceMemP introducedGMIds) members
|
||||
introduceMember vr user gInfo m recipients Nothing
|
||||
where
|
||||
introduceMemP introducedGMIds mem =
|
||||
memberCurrent mem
|
||||
&& groupMemberId' mem `notElem` introducedGMIds
|
||||
&& groupMemberId' mem /= groupMemberId' m
|
||||
|
||||
introduceMember :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> [GroupMember] -> Maybe MsgScope -> CM ()
|
||||
introduceMember _ _ _ GroupMember {activeConn = Nothing} _ _ = throwChatError $ CEInternalError "member connection not active"
|
||||
introduceMember vr user gInfo@GroupInfo {groupId} m@GroupMember {activeConn = Just conn} introduceToMembers msgScope = do
|
||||
void . sendGroupMessage' user gInfo introduceToMembers $ XGrpMemNew (memberInfo m) msgScope
|
||||
sendIntroductions introduceToMembers
|
||||
void . sendGroupMessage user gInfo members . XGrpMemNew $ memberInfo m
|
||||
sendIntroductions members
|
||||
when (groupFeatureAllowed SGFHistory gInfo) sendHistory
|
||||
where
|
||||
sendIntroductions members = do
|
||||
intros <- withStore' $ \db -> createIntroductions db (maxVersion vr) members m
|
||||
|
@ -1031,7 +1003,7 @@ introduceMember vr user gInfo@GroupInfo {groupId} m@GroupMember {activeConn = Ju
|
|||
memberIntro reMember =
|
||||
let mInfo = memberInfo reMember
|
||||
mRestrictions = memberRestrictions reMember
|
||||
in XGrpMemIntro mInfo mRestrictions
|
||||
in XGrpMemIntro mInfo mRestrictions
|
||||
shuffleIntros :: [GroupMemberIntro] -> IO [GroupMemberIntro]
|
||||
shuffleIntros intros = do
|
||||
let (admins, others) = partition isAdmin intros
|
||||
|
@ -1044,19 +1016,15 @@ introduceMember vr user gInfo@GroupInfo {groupId} m@GroupMember {activeConn = Ju
|
|||
processIntro intro@GroupMemberIntro {introId} = do
|
||||
void $ sendDirectMemberMessage conn (memberIntro $ reMember intro) groupId
|
||||
withStore' $ \db -> updateIntroStatus db introId GMIntroSent
|
||||
|
||||
sendHistory :: User -> GroupInfo -> GroupMember -> CM ()
|
||||
sendHistory _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active"
|
||||
sendHistory user gInfo@GroupInfo {groupId, membership} m@GroupMember {activeConn = Just conn} =
|
||||
when (m `supportsVersion` batchSendVersion) $ do
|
||||
(errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo m 100)
|
||||
(errs', events) <- partitionEithers <$> mapM (tryChatError . itemForwardEvents) items
|
||||
let errors = map ChatErrorStore errs <> errs'
|
||||
unless (null errors) $ toView $ CEvtChatErrors errors
|
||||
let events' = maybe (concat events) (\x -> concat events <> [x]) descrEvent_
|
||||
forM_ (L.nonEmpty events') $ \events'' ->
|
||||
sendGroupMemberMessages user conn events'' groupId
|
||||
where
|
||||
sendHistory =
|
||||
when (m `supportsVersion` batchSendVersion) $ do
|
||||
(errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo m 100)
|
||||
(errs', events) <- partitionEithers <$> mapM (tryChatError . itemForwardEvents) items
|
||||
let errors = map ChatErrorStore errs <> errs'
|
||||
unless (null errors) $ toView $ CEvtChatErrors errors
|
||||
let events' = maybe (concat events) (\x -> concat events <> [x]) descrEvent_
|
||||
forM_ (L.nonEmpty events') $ \events'' ->
|
||||
sendGroupMemberMessages user conn events'' groupId
|
||||
descrEvent_ :: Maybe (ChatMsgEvent 'Json)
|
||||
descrEvent_
|
||||
| m `supportsVersion` groupHistoryIncludeWelcomeVersion = do
|
||||
|
@ -1102,7 +1070,7 @@ sendHistory user gInfo@GroupInfo {groupId, membership} m@GroupMember {activeConn
|
|||
| fileDescrComplete =
|
||||
let fInvDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False}
|
||||
fInv = xftpFileInvitation fileName fileSize fInvDescr
|
||||
in Just (fInv, fileDescrText)
|
||||
in Just (fInv, fileDescrText)
|
||||
| otherwise = Nothing
|
||||
processContentItem :: GroupMember -> ChatItem 'CTGroup d -> MsgContent -> Maybe (FileInvitation, RcvFileDescrText) -> CM [ChatMsgEvent 'Json]
|
||||
processContentItem sender ChatItem {formattedText, meta, quotedItem, mentions} mc fInvDescr_ =
|
||||
|
@ -1114,8 +1082,7 @@ sendHistory user gInfo@GroupInfo {groupId, membership} m@GroupMember {activeConn
|
|||
fInv_ = fst <$> fInvDescr_
|
||||
(mc', _, mentions') = updatedMentionNames mc formattedText mentions
|
||||
mentions'' = M.map (\CIMention {memberId} -> MsgMention {memberId}) mentions'
|
||||
-- TODO [knocking] send history to other scopes too?
|
||||
(chatMsgEvent, _) <- withStore $ \db -> prepareGroupMsg db user gInfo Nothing mc' mentions'' quotedItemId_ Nothing fInv_ itemTimed False
|
||||
(chatMsgEvent, _) <- withStore $ \db -> prepareGroupMsg db user gInfo mc' mentions'' quotedItemId_ Nothing fInv_ itemTimed False
|
||||
let senderVRange = memberChatVRange' sender
|
||||
xMsgNewChatMsg = ChatMessage {chatVRange = senderVRange, msgId = itemSharedMsgId, chatMsgEvent}
|
||||
fileDescrEvents <- case (snd <$> fInvDescr_, itemSharedMsgId) of
|
||||
|
@ -1180,7 +1147,7 @@ startTimedItemThread user itemRef deleteAt = do
|
|||
atomically $ writeTVar threadTVar (Just tId)
|
||||
|
||||
deleteTimedItem :: User -> (ChatRef, ChatItemId) -> UTCTime -> CM ()
|
||||
deleteTimedItem user (ChatRef cType chatId scope, itemId) deleteAt = do
|
||||
deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do
|
||||
ts <- liftIO getCurrentTime
|
||||
liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime deleteAt ts
|
||||
lift waitChatStartedAndActivated
|
||||
|
@ -1193,8 +1160,7 @@ deleteTimedItem user (ChatRef cType chatId scope, itemId) deleteAt = do
|
|||
CTGroup -> do
|
||||
(gInfo, ci) <- withStore $ \db -> (,) <$> getGroupInfo db vr user chatId <*> getGroupChatItem db user chatId itemId
|
||||
deletedTs <- liftIO getCurrentTime
|
||||
chatScopeInfo <- mapM (getChatScopeInfo vr user) scope
|
||||
deletions <- deleteGroupCIs user gInfo chatScopeInfo [ci] Nothing deletedTs
|
||||
deletions <- deleteGroupCIs user gInfo [ci] Nothing deletedTs
|
||||
toView $ CEvtChatItemsDeleted user deletions True True
|
||||
_ -> eToView $ ChatError $ CEInternalError "bad deleteTimedItem cType"
|
||||
|
||||
|
@ -1310,103 +1276,6 @@ parseChatMessage conn s = do
|
|||
errType = CEInvalidChatMessage conn Nothing (safeDecodeUtf8 s)
|
||||
{-# INLINE parseChatMessage #-}
|
||||
|
||||
getChatScopeInfo :: VersionRangeChat -> User -> GroupChatScope -> CM GroupChatScopeInfo
|
||||
getChatScopeInfo vr user = \case
|
||||
GCSMemberSupport Nothing -> pure $ GCSIMemberSupport Nothing
|
||||
GCSMemberSupport (Just gmId) -> do
|
||||
supportMem <- withFastStore $ \db -> getGroupMemberById db vr user gmId
|
||||
pure $ GCSIMemberSupport (Just supportMem)
|
||||
|
||||
-- TODO [knocking] refactor to GroupChatScope -> "a" function, "a" is some new type? Or possibly split to get scope/get recipients steps
|
||||
getGroupRecipients :: VersionRangeChat -> User -> GroupInfo -> Maybe GroupChatScope -> VersionChat -> CM (Maybe GroupChatScopeInfo, [GroupMember])
|
||||
getGroupRecipients vr user gInfo@GroupInfo {membership} scope modsCompatVersion = case scope of
|
||||
Nothing -> do
|
||||
unless (memberCurrent membership && memberActive membership) $ throwChatError $ CECommandError "not current member"
|
||||
ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo
|
||||
let recipients = filter memberCurrent ms
|
||||
pure (Nothing, recipients)
|
||||
Just (GCSMemberSupport Nothing) -> do
|
||||
modMs <- withFastStore' $ \db -> getGroupModerators db vr user gInfo
|
||||
let rcpModMs' = filter (\m -> compatible m && memberCurrent m) modMs
|
||||
when (null rcpModMs') $ throwChatError $ CECommandError "no admins support this message"
|
||||
let scopeInfo = GCSIMemberSupport Nothing
|
||||
pure (Just scopeInfo, rcpModMs')
|
||||
Just (GCSMemberSupport (Just gmId)) -> do
|
||||
unless (memberCurrent membership && memberActive membership) $ throwChatError $ CECommandError "not current member"
|
||||
supportMem <- withFastStore $ \db -> getGroupMemberById db vr user gmId
|
||||
unless (memberCurrentOrPending supportMem) $ throwChatError $ CECommandError "support member not current or pending"
|
||||
let scopeInfo = GCSIMemberSupport (Just supportMem)
|
||||
if memberStatus supportMem == GSMemPendingApproval
|
||||
then pure (Just scopeInfo, [supportMem])
|
||||
else do
|
||||
modMs <- withFastStore' $ \db -> getGroupModerators db vr user gInfo
|
||||
let rcpModMs' = filter (\m -> compatible m && memberCurrent m) modMs
|
||||
pure (Just scopeInfo, [supportMem] <> rcpModMs')
|
||||
where
|
||||
compatible GroupMember {activeConn, memberChatVRange} =
|
||||
maxVersion (maybe memberChatVRange peerChatVRange activeConn) >= modsCompatVersion
|
||||
|
||||
mkLocalGroupChatScope :: GroupInfo -> CM (GroupInfo, Maybe GroupChatScopeInfo)
|
||||
mkLocalGroupChatScope gInfo@GroupInfo {membership}
|
||||
| memberPending membership = do
|
||||
(gInfo', scopeInfo) <- mkGroupSupportChatInfo gInfo
|
||||
pure (gInfo', Just scopeInfo)
|
||||
| otherwise =
|
||||
pure (gInfo, Nothing)
|
||||
|
||||
mkGroupChatScope :: GroupInfo -> GroupMember -> CM (GroupInfo, GroupMember, Maybe GroupChatScopeInfo)
|
||||
mkGroupChatScope gInfo@GroupInfo {membership} m
|
||||
| memberPending membership = do
|
||||
(gInfo', scopeInfo) <- mkGroupSupportChatInfo gInfo
|
||||
pure (gInfo', m, Just scopeInfo)
|
||||
| memberPending m = do
|
||||
(m', scopeInfo) <- mkMemberSupportChatInfo m
|
||||
pure (gInfo, m', Just scopeInfo)
|
||||
| otherwise =
|
||||
pure (gInfo, m, Nothing)
|
||||
|
||||
mkGetMessageChatScope :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> Maybe MsgScope -> CM (GroupInfo, GroupMember, Maybe GroupChatScopeInfo)
|
||||
mkGetMessageChatScope vr user gInfo@GroupInfo {membership} m msgScope_ =
|
||||
mkGroupChatScope gInfo m >>= \case
|
||||
groupScope@(_gInfo', _m', Just _scopeInfo) -> pure groupScope
|
||||
(_, _, Nothing) -> case msgScope_ of
|
||||
Nothing -> pure (gInfo, m, Nothing)
|
||||
Just (MSMember mId)
|
||||
| sameMemberId mId membership -> do
|
||||
(gInfo', scopeInfo) <- mkGroupSupportChatInfo gInfo
|
||||
pure (gInfo', m, Just scopeInfo)
|
||||
| otherwise -> do
|
||||
referredMember <- withStore $ \db -> getGroupMemberByMemberId db vr user gInfo mId
|
||||
-- TODO [knocking] return patched _referredMember' too?
|
||||
(_referredMember', scopeInfo) <- mkMemberSupportChatInfo referredMember
|
||||
pure (gInfo, m, Just scopeInfo)
|
||||
|
||||
mkGroupSupportChatInfo :: GroupInfo -> CM (GroupInfo, GroupChatScopeInfo)
|
||||
mkGroupSupportChatInfo gInfo@GroupInfo {membership} =
|
||||
case supportChat membership of
|
||||
Nothing -> do
|
||||
chatTs <- liftIO getCurrentTime
|
||||
withStore' $ \db -> setSupportChatTs db (groupMemberId' membership) chatTs
|
||||
let gInfo' = gInfo {membership = membership {supportChat = Just $ GroupSupportChat chatTs 0 0 0 Nothing}}
|
||||
scopeInfo = GCSIMemberSupport {groupMember_ = Nothing}
|
||||
pure (gInfo', scopeInfo)
|
||||
Just _supportChat ->
|
||||
let scopeInfo = GCSIMemberSupport {groupMember_ = Nothing}
|
||||
in pure (gInfo, scopeInfo)
|
||||
|
||||
mkMemberSupportChatInfo :: GroupMember -> CM (GroupMember, GroupChatScopeInfo)
|
||||
mkMemberSupportChatInfo m@GroupMember {groupMemberId, supportChat} =
|
||||
case supportChat of
|
||||
Nothing -> do
|
||||
chatTs <- liftIO getCurrentTime
|
||||
withStore' $ \db -> setSupportChatTs db groupMemberId chatTs
|
||||
let m' = m {supportChat = Just $ GroupSupportChat chatTs 0 0 0 Nothing}
|
||||
scopeInfo = GCSIMemberSupport {groupMember_ = Just m'}
|
||||
pure (m', scopeInfo)
|
||||
Just _supportChat ->
|
||||
let scopeInfo = GCSIMemberSupport {groupMember_ = Just m}
|
||||
in pure (m, scopeInfo)
|
||||
|
||||
sendFileChunk :: User -> SndFileTransfer -> CM ()
|
||||
sendFileChunk user ft@SndFileTransfer {fileId, fileStatus, agentConnId = AgentConnId acId} =
|
||||
unless (fileStatus == FSComplete || fileStatus == FSCancelled) $ do
|
||||
|
@ -1564,20 +1433,15 @@ deleteMemberConnection' GroupMember {activeConn} waitDelivery = do
|
|||
deleteAgentConnectionAsync' (aConnId conn) waitDelivery
|
||||
withStore' $ \db -> updateConnectionStatus db conn ConnDeleted
|
||||
|
||||
deleteOrUpdateMemberRecord :: User -> GroupInfo -> GroupMember -> CM GroupInfo
|
||||
deleteOrUpdateMemberRecord user gInfo member =
|
||||
withStore' $ \db -> deleteOrUpdateMemberRecordIO db user gInfo member
|
||||
deleteOrUpdateMemberRecord :: User -> GroupMember -> CM ()
|
||||
deleteOrUpdateMemberRecord user member =
|
||||
withStore' $ \db -> deleteOrUpdateMemberRecordIO db user member
|
||||
|
||||
deleteOrUpdateMemberRecordIO :: DB.Connection -> User -> GroupInfo -> GroupMember -> IO GroupInfo
|
||||
deleteOrUpdateMemberRecordIO db user@User {userId} gInfo member = do
|
||||
gInfo' <-
|
||||
if gmRequiresAttention member
|
||||
then decreaseGroupMembersRequireAttention db user gInfo
|
||||
else pure gInfo
|
||||
deleteOrUpdateMemberRecordIO :: DB.Connection -> User -> GroupMember -> IO ()
|
||||
deleteOrUpdateMemberRecordIO db user@User {userId} member =
|
||||
checkGroupMemberHasItems db user member >>= \case
|
||||
Just _ -> updateGroupMemberStatus db userId member GSMemRemoved
|
||||
Nothing -> deleteGroupMember db user member
|
||||
pure gInfo'
|
||||
|
||||
sendDirectContactMessages :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM [Either ChatError SndMessage]
|
||||
sendDirectContactMessages user ct events = do
|
||||
|
@ -1764,9 +1628,9 @@ deliverMessagesB msgReqs = do
|
|||
where
|
||||
updatePQ = updateConnPQSndEnabled db connId pqSndEnabled'
|
||||
|
||||
sendGroupMessage :: MsgEncodingI e => User -> GroupInfo -> Maybe GroupChatScope -> [GroupMember] -> ChatMsgEvent e -> CM SndMessage
|
||||
sendGroupMessage user gInfo gcScope members chatMsgEvent = do
|
||||
sendGroupMessages user gInfo gcScope members (chatMsgEvent :| []) >>= \case
|
||||
sendGroupMessage :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM SndMessage
|
||||
sendGroupMessage user gInfo members chatMsgEvent = do
|
||||
sendGroupMessages user gInfo members (chatMsgEvent :| []) >>= \case
|
||||
((Right msg) :| [], _) -> pure msg
|
||||
_ -> throwChatError $ CEInternalError "sendGroupMessage: expected 1 message"
|
||||
|
||||
|
@ -1776,9 +1640,9 @@ sendGroupMessage' user gInfo members chatMsgEvent =
|
|||
((Right msg) :| [], _) -> pure msg
|
||||
_ -> throwChatError $ CEInternalError "sendGroupMessage': expected 1 message"
|
||||
|
||||
sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> Maybe GroupChatScope -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult)
|
||||
sendGroupMessages user gInfo scope members events = do
|
||||
-- TODO [knocking] send current profile to pending member after approval?
|
||||
sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult)
|
||||
sendGroupMessages user gInfo members events = do
|
||||
-- TODO [knocking] when sending to all, send profile update to pending approval members too, then filter for next step?
|
||||
when shouldSendProfileUpdate $
|
||||
sendProfileUpdate `catchChatError` eToView
|
||||
sendGroupMessages_ user gInfo members events
|
||||
|
@ -1786,7 +1650,6 @@ sendGroupMessages user gInfo scope members events = do
|
|||
User {profile = p, userMemberProfileUpdatedAt} = user
|
||||
GroupInfo {userMemberProfileSentAt} = gInfo
|
||||
shouldSendProfileUpdate
|
||||
| isJust scope = False -- why not sending profile updates to scopes?
|
||||
| incognitoMembership gInfo = False
|
||||
| otherwise =
|
||||
case (userMemberProfileSentAt, userMemberProfileUpdatedAt) of
|
||||
|
@ -1807,13 +1670,16 @@ data GroupSndResult = GroupSndResult
|
|||
}
|
||||
|
||||
sendGroupMessages_ :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult)
|
||||
sendGroupMessages_ _user gInfo@GroupInfo {groupId} recipientMembers events = do
|
||||
sendGroupMessages_ _user gInfo@GroupInfo {groupId} members events = do
|
||||
let idsEvts = L.map (GroupId groupId,) events
|
||||
sndMsgs_ <- lift $ createSndMessages idsEvts
|
||||
recipientMembers' <- liftIO $ shuffleMembers recipientMembers
|
||||
-- TODO [knocking] Possibly we need to pass GroupSndScope through all functions to here to avoid ad-hoc filtering.
|
||||
recipientMembers <- case members of
|
||||
[m] | memberStatus m == GSMemPendingApproval -> pure [m]
|
||||
_ -> liftIO $ shuffleMembers (filter memberCurrent members)
|
||||
let msgFlags = MsgFlags {notification = any (hasNotification . toCMEventTag) events}
|
||||
(toSendSeparate, toSendBatched, toPending, forwarded, _, dups) =
|
||||
foldr' (addMember recipientMembers') ([], [], [], [], S.empty, 0 :: Int) recipientMembers'
|
||||
foldr' addMember ([], [], [], [], S.empty, 0 :: Int) recipientMembers
|
||||
when (dups /= 0) $ logError $ "sendGroupMessages_: " <> tshow dups <> " duplicate members"
|
||||
-- TODO PQ either somehow ensure that group members connections cannot have pqSupport/pqEncryption or pass Off's here
|
||||
-- Deliver to toSend members
|
||||
|
@ -1835,7 +1701,7 @@ sendGroupMessages_ _user gInfo@GroupInfo {groupId} recipientMembers events = do
|
|||
liftM2 (<>) (shuffle adminMs) (shuffle otherMs)
|
||||
where
|
||||
isAdmin GroupMember {memberRole} = memberRole >= GRAdmin
|
||||
addMember members m acc@(toSendSeparate, toSendBatched, pending, forwarded, !mIds, !dups) =
|
||||
addMember m acc@(toSendSeparate, toSendBatched, pending, forwarded, !mIds, !dups) =
|
||||
case memberSendAction gInfo events members m of
|
||||
Just a
|
||||
| mId `S.member` mIds -> (toSendSeparate, toSendBatched, pending, forwarded, mIds, dups + 1)
|
||||
|
@ -2012,7 +1878,7 @@ saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothi
|
|||
saveSndChatItem' :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> Maybe (CIFile 'MDSnd) -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> CM (ChatItem c 'MDSnd)
|
||||
saveSndChatItem' user cd msg content ciFile quotedItem itemForwarded itemTimed live = do
|
||||
let itemTexts = ciContentTexts content
|
||||
saveSndChatItems user cd [Right NewSndChatItemData {msg, content, itemTexts, itemMentions = M.empty, ciFile, quotedItem, itemForwarded}] itemTimed live >>= \case
|
||||
saveSndChatItems user cd Nothing [Right NewSndChatItemData {msg, content, itemTexts, itemMentions = M.empty, ciFile, quotedItem, itemForwarded}] itemTimed live >>= \case
|
||||
[Right ci] -> pure ci
|
||||
_ -> throwChatError $ CEInternalError "saveSndChatItem': expected 1 item"
|
||||
|
||||
|
@ -2031,43 +1897,43 @@ saveSndChatItems ::
|
|||
ChatTypeI c =>
|
||||
User ->
|
||||
ChatDirection c 'MDSnd ->
|
||||
Maybe NotInHistory ->
|
||||
[Either ChatError (NewSndChatItemData c)] ->
|
||||
Maybe CITimed ->
|
||||
Bool ->
|
||||
CM [Either ChatError (ChatItem c 'MDSnd)]
|
||||
saveSndChatItems user cd itemsData itemTimed live = do
|
||||
saveSndChatItems user cd notInHistory_ itemsData itemTimed live = do
|
||||
createdAt <- liftIO getCurrentTime
|
||||
vr <- chatVersionRange
|
||||
when (contactChatDeleted cd || any (\NewSndChatItemData {content} -> ciRequiresAttention content) (rights itemsData)) $
|
||||
void $ withStore' (\db -> updateChatTsStats db vr user cd createdAt Nothing)
|
||||
withStore' (\db -> updateChatTs db user cd createdAt)
|
||||
lift $ withStoreBatch (\db -> map (bindRight $ createItem db createdAt) itemsData)
|
||||
where
|
||||
createItem :: DB.Connection -> UTCTime -> NewSndChatItemData c -> IO (Either ChatError (ChatItem c 'MDSnd))
|
||||
createItem db createdAt NewSndChatItemData {msg = msg@SndMessage {sharedMsgId}, content, itemTexts, itemMentions, ciFile, quotedItem, itemForwarded} = do
|
||||
ciId <- createNewSndChatItem db user cd msg content quotedItem itemForwarded itemTimed live createdAt
|
||||
ciId <- createNewSndChatItem db user cd notInHistory_ msg content quotedItem itemForwarded itemTimed live createdAt
|
||||
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
|
||||
let ci = mkChatItem_ cd ciId content itemTexts ciFile quotedItem (Just sharedMsgId) itemForwarded itemTimed live False createdAt Nothing createdAt
|
||||
Right <$> case cd of
|
||||
CDGroupSnd g _scope | not (null itemMentions) -> createGroupCIMentions db g ci itemMentions
|
||||
CDGroupSnd g | not (null itemMentions) -> createGroupCIMentions db g ci itemMentions
|
||||
_ -> pure ci
|
||||
|
||||
saveRcvChatItemNoParse :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> CM (ChatItem c 'MDRcv, ChatInfo c)
|
||||
saveRcvChatItemNoParse :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> CIContent 'MDRcv -> CM (ChatItem c 'MDRcv)
|
||||
saveRcvChatItemNoParse user cd msg brokerTs = saveRcvChatItem user cd msg brokerTs . ciContentNoParse
|
||||
|
||||
saveRcvChatItem :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) -> CM (ChatItem c 'MDRcv, ChatInfo c)
|
||||
saveRcvChatItem :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> UTCTime -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) -> CM (ChatItem c 'MDRcv)
|
||||
saveRcvChatItem user cd msg@RcvMessage {sharedMsgId_} brokerTs content =
|
||||
saveRcvChatItem' user cd msg sharedMsgId_ brokerTs content Nothing Nothing False M.empty
|
||||
saveRcvChatItem' user cd Nothing msg sharedMsgId_ brokerTs content Nothing Nothing False M.empty
|
||||
|
||||
ciContentNoParse :: CIContent 'MDRcv -> (CIContent 'MDRcv, (Text, Maybe MarkdownList))
|
||||
ciContentNoParse content = (content, (ciContentToText content, Nothing))
|
||||
|
||||
saveRcvChatItem' :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> UTCTime -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) -> Maybe (CIFile 'MDRcv) -> Maybe CITimed -> Bool -> Map MemberName MsgMention -> CM (ChatItem c 'MDRcv, ChatInfo c)
|
||||
saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, forwardedByMember} sharedMsgId_ brokerTs (content, (t, ft_)) ciFile itemTimed live mentions = do
|
||||
saveRcvChatItem' :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> Maybe NotInHistory -> RcvMessage -> Maybe SharedMsgId -> UTCTime -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) -> Maybe (CIFile 'MDRcv) -> Maybe CITimed -> Bool -> Map MemberName MsgMention -> CM (ChatItem c 'MDRcv)
|
||||
saveRcvChatItem' user cd notInHistory_ msg@RcvMessage {chatMsgEvent, forwardedByMember} sharedMsgId_ brokerTs (content, (t, ft_)) ciFile itemTimed live mentions = do
|
||||
createdAt <- liftIO getCurrentTime
|
||||
vr <- chatVersionRange
|
||||
withStore' $ \db -> do
|
||||
when (ciRequiresAttention content || contactChatDeleted cd) $ updateChatTs db user cd createdAt
|
||||
(mentions' :: Map MemberName CIMention, userMention) <- case cd of
|
||||
CDGroupRcv g@GroupInfo {membership} _scope _m -> do
|
||||
CDGroupRcv g@GroupInfo {membership} _ -> do
|
||||
mentions' <- getRcvCIMentions db user g ft_ mentions
|
||||
let userReply = case cmToQuotedMsg chatMsgEvent of
|
||||
Just QuotedMsg {msgRef = MsgRef {memberId = Just mId}} -> sameMemberId mId membership
|
||||
|
@ -2075,23 +1941,12 @@ saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, forwardedByMember} shared
|
|||
userMention' = userReply || any (\CIMention {memberId} -> sameMemberId memberId membership) mentions'
|
||||
in pure (mentions', userMention')
|
||||
CDDirectRcv _ -> pure (M.empty, False)
|
||||
cInfo' <- if (ciRequiresAttention content || contactChatDeleted cd)
|
||||
then updateChatTsStats db vr user cd createdAt (memberChatStats userMention)
|
||||
else pure $ toChatInfo cd
|
||||
(ciId, quotedItem, itemForwarded) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live userMention brokerTs createdAt
|
||||
(ciId, quotedItem, itemForwarded) <- createNewRcvChatItem db user cd notInHistory_ msg sharedMsgId_ content itemTimed live userMention brokerTs createdAt
|
||||
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
|
||||
let ci = mkChatItem_ cd ciId content (t, ft_) ciFile quotedItem sharedMsgId_ itemForwarded itemTimed live userMention brokerTs forwardedByMember createdAt
|
||||
ci' <- case cd of
|
||||
CDGroupRcv g _scope _m | not (null mentions') -> createGroupCIMentions db g ci mentions'
|
||||
case cd of
|
||||
CDGroupRcv g _ | not (null mentions') -> createGroupCIMentions db g ci mentions'
|
||||
_ -> pure ci
|
||||
pure (ci', cInfo')
|
||||
where
|
||||
memberChatStats :: Bool -> Maybe (Int, MemberAttention, Int)
|
||||
memberChatStats userMention = case cd of
|
||||
CDGroupRcv _g (Just scope) m -> do
|
||||
let unread = fromEnum $ ciCreateStatus content == CISRcvNew
|
||||
in Just (unread, memberAttentionChange unread (Just brokerTs) m scope, fromEnum userMention)
|
||||
_ -> Nothing
|
||||
|
||||
-- TODO [mentions] optimize by avoiding unnecessary parsing
|
||||
mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d
|
||||
|
@ -2307,39 +2162,19 @@ createInternalItemsForChats ::
|
|||
createInternalItemsForChats user itemTs_ dirsCIContents = do
|
||||
createdAt <- liftIO getCurrentTime
|
||||
let itemTs = fromMaybe createdAt itemTs_
|
||||
vr <- chatVersionRange'
|
||||
void . withStoreBatch' $ \db -> map (uncurry $ updateChat db vr createdAt) dirsCIContents
|
||||
void . withStoreBatch' $ \db -> map (uncurry $ updateChat db createdAt) dirsCIContents
|
||||
withStoreBatch' $ \db -> concatMap (uncurry $ createACIs db itemTs createdAt) dirsCIContents
|
||||
where
|
||||
updateChat :: DB.Connection -> VersionRangeChat -> UTCTime -> ChatDirection c d -> [CIContent d] -> IO ()
|
||||
updateChat db vr createdAt cd contents
|
||||
| any ciRequiresAttention contents || contactChatDeleted cd = void $ updateChatTsStats db vr user cd createdAt memberChatStats
|
||||
updateChat :: DB.Connection -> UTCTime -> ChatDirection c d -> [CIContent d] -> IO ()
|
||||
updateChat db createdAt cd contents
|
||||
| any ciRequiresAttention contents || contactChatDeleted cd = updateChatTs db user cd createdAt
|
||||
| otherwise = pure ()
|
||||
where
|
||||
memberChatStats :: Maybe (Int, MemberAttention, Int)
|
||||
memberChatStats = case cd of
|
||||
CDGroupRcv _g (Just scope) m -> do
|
||||
let unread = length $ filter ciRequiresAttention contents
|
||||
in Just (unread, memberAttentionChange unread itemTs_ m scope, 0)
|
||||
_ -> Nothing
|
||||
createACIs :: DB.Connection -> UTCTime -> UTCTime -> ChatDirection c d -> [CIContent d] -> [IO AChatItem]
|
||||
createACIs db itemTs createdAt cd = map $ \content -> do
|
||||
ciId <- createNewChatItemNoMsg db user cd content itemTs createdAt
|
||||
let ci = mkChatItem cd ciId content Nothing Nothing Nothing Nothing Nothing False False itemTs Nothing createdAt
|
||||
pure $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci
|
||||
|
||||
memberAttentionChange :: Int -> (Maybe UTCTime) -> GroupMember -> GroupChatScopeInfo -> MemberAttention
|
||||
memberAttentionChange unread brokerTs_ rcvMem = \case
|
||||
GCSIMemberSupport (Just suppMem)
|
||||
| groupMemberId' suppMem == groupMemberId' rcvMem -> MAInc unread brokerTs_
|
||||
| msgIsNewerThanLastUnanswered -> MAReset
|
||||
| otherwise -> MAInc 0 Nothing
|
||||
where
|
||||
msgIsNewerThanLastUnanswered = case (supportChat suppMem >>= lastMsgFromMemberTs, brokerTs_) of
|
||||
(Just lastMsgTs, Just brokerTs) -> lastMsgTs < brokerTs
|
||||
_ -> False
|
||||
GCSIMemberSupport Nothing -> MAInc 0 Nothing
|
||||
|
||||
createLocalChatItems ::
|
||||
User ->
|
||||
ChatDirection 'CTLocal 'MDSnd ->
|
||||
|
@ -2347,15 +2182,14 @@ createLocalChatItems ::
|
|||
UTCTime ->
|
||||
CM [ChatItem 'CTLocal 'MDSnd]
|
||||
createLocalChatItems user cd itemsData createdAt = do
|
||||
vr <- chatVersionRange
|
||||
void $ withStore' $ \db -> updateChatTsStats db vr user cd createdAt Nothing
|
||||
withStore' $ \db -> updateChatTs db user cd createdAt
|
||||
(errs, items) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (createItem db) $ L.toList itemsData)
|
||||
unless (null errs) $ toView $ CEvtChatErrors errs
|
||||
pure items
|
||||
where
|
||||
createItem :: DB.Connection -> (CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom, (Text, Maybe MarkdownList)) -> IO (ChatItem 'CTLocal 'MDSnd)
|
||||
createItem db (content, ciFile, itemForwarded, ts) = do
|
||||
ciId <- createNewChatItem_ db user cd Nothing Nothing content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False False createdAt Nothing createdAt
|
||||
ciId <- createNewChatItem_ db user cd Nothing Nothing Nothing content (Nothing, Nothing, Nothing, Nothing, Nothing) itemForwarded Nothing False False createdAt Nothing createdAt
|
||||
forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt
|
||||
pure $ mkChatItem_ cd ciId content ts ciFile Nothing Nothing itemForwarded Nothing False False createdAt Nothing createdAt
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -61,25 +61,6 @@ import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
|
|||
data ChatType = CTDirect | CTGroup | CTLocal | CTContactRequest | CTContactConnection
|
||||
deriving (Eq, Show, Ord)
|
||||
|
||||
data GroupChatScope
|
||||
= GCSMemberSupport {groupMemberId_ :: Maybe GroupMemberId} -- Nothing means own conversation with support
|
||||
deriving (Eq, Show, Ord)
|
||||
|
||||
data GroupChatScopeTag
|
||||
= GCSTMemberSupport_
|
||||
deriving (Eq, Show)
|
||||
|
||||
instance FromField GroupChatScopeTag where fromField = fromTextField_ textDecode
|
||||
|
||||
instance ToField GroupChatScopeTag where toField = toField . textEncode
|
||||
|
||||
instance TextEncoding GroupChatScopeTag where
|
||||
textDecode = \case
|
||||
"member_support" -> Just GCSTMemberSupport_
|
||||
_ -> Nothing
|
||||
textEncode = \case
|
||||
GCSTMemberSupport_ -> "member_support"
|
||||
|
||||
data ChatName = ChatName {chatType :: ChatType, chatName :: Text}
|
||||
deriving (Show)
|
||||
|
||||
|
@ -94,52 +75,44 @@ chatTypeStr = \case
|
|||
chatNameStr :: ChatName -> String
|
||||
chatNameStr (ChatName cType name) = T.unpack $ chatTypeStr cType <> if T.any isSpace name then "'" <> name <> "'" else name
|
||||
|
||||
data ChatRef = ChatRef ChatType Int64 (Maybe GroupChatScope)
|
||||
data ChatRef = ChatRef ChatType Int64
|
||||
deriving (Eq, Show, Ord)
|
||||
|
||||
data ChatInfo (c :: ChatType) where
|
||||
DirectChat :: Contact -> ChatInfo 'CTDirect
|
||||
GroupChat :: GroupInfo -> Maybe GroupChatScopeInfo -> ChatInfo 'CTGroup
|
||||
GroupChat :: GroupInfo -> ChatInfo 'CTGroup
|
||||
LocalChat :: NoteFolder -> ChatInfo 'CTLocal
|
||||
ContactRequest :: UserContactRequest -> ChatInfo 'CTContactRequest
|
||||
ContactConnection :: PendingContactConnection -> ChatInfo 'CTContactConnection
|
||||
|
||||
deriving instance Show (ChatInfo c)
|
||||
|
||||
data GroupChatScopeInfo
|
||||
= GCSIMemberSupport {groupMember_ :: Maybe GroupMember}
|
||||
deriving (Show)
|
||||
|
||||
toChatScope :: GroupChatScopeInfo -> GroupChatScope
|
||||
toChatScope = \case
|
||||
GCSIMemberSupport {groupMember_} -> GCSMemberSupport $ groupMemberId' <$> groupMember_
|
||||
|
||||
toMsgScope :: GroupInfo -> GroupChatScopeInfo -> MsgScope
|
||||
toMsgScope GroupInfo {membership} = \case
|
||||
GCSIMemberSupport {groupMember_} -> MSMember $ memberId' $ fromMaybe membership groupMember_
|
||||
chatInfoChatTs :: ChatInfo c -> Maybe UTCTime
|
||||
chatInfoChatTs = \case
|
||||
DirectChat Contact {chatTs} -> chatTs
|
||||
GroupChat GroupInfo {chatTs} -> chatTs
|
||||
_ -> Nothing
|
||||
|
||||
chatInfoToRef :: ChatInfo c -> ChatRef
|
||||
chatInfoToRef = \case
|
||||
DirectChat Contact {contactId} -> ChatRef CTDirect contactId Nothing
|
||||
GroupChat GroupInfo {groupId} scopeInfo -> ChatRef CTGroup groupId (toChatScope <$> scopeInfo)
|
||||
LocalChat NoteFolder {noteFolderId} -> ChatRef CTLocal noteFolderId Nothing
|
||||
ContactRequest UserContactRequest {contactRequestId} -> ChatRef CTContactRequest contactRequestId Nothing
|
||||
ContactConnection PendingContactConnection {pccConnId} -> ChatRef CTContactConnection pccConnId Nothing
|
||||
DirectChat Contact {contactId} -> ChatRef CTDirect contactId
|
||||
GroupChat GroupInfo {groupId} -> ChatRef CTGroup groupId
|
||||
LocalChat NoteFolder {noteFolderId} -> ChatRef CTLocal noteFolderId
|
||||
ContactRequest UserContactRequest {contactRequestId} -> ChatRef CTContactRequest contactRequestId
|
||||
ContactConnection PendingContactConnection {pccConnId} -> ChatRef CTContactConnection pccConnId
|
||||
|
||||
chatInfoMembership :: ChatInfo c -> Maybe GroupMember
|
||||
chatInfoMembership = \case
|
||||
GroupChat GroupInfo {membership} _scopeInfo -> Just membership
|
||||
GroupChat GroupInfo {membership} -> Just membership
|
||||
_ -> Nothing
|
||||
|
||||
data JSONChatInfo
|
||||
= JCInfoDirect {contact :: Contact}
|
||||
| JCInfoGroup {groupInfo :: GroupInfo, groupChatScope :: Maybe GroupChatScopeInfo}
|
||||
| JCInfoGroup {groupInfo :: GroupInfo}
|
||||
| JCInfoLocal {noteFolder :: NoteFolder}
|
||||
| JCInfoContactRequest {contactRequest :: UserContactRequest}
|
||||
| JCInfoContactConnection {contactConnection :: PendingContactConnection}
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GCSI") ''GroupChatScopeInfo)
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCInfo") ''JSONChatInfo)
|
||||
|
||||
instance ChatTypeI c => FromJSON (ChatInfo c) where
|
||||
|
@ -152,7 +125,7 @@ instance ToJSON (ChatInfo c) where
|
|||
jsonChatInfo :: ChatInfo c -> JSONChatInfo
|
||||
jsonChatInfo = \case
|
||||
DirectChat c -> JCInfoDirect c
|
||||
GroupChat g s -> JCInfoGroup g s
|
||||
GroupChat g -> JCInfoGroup g
|
||||
LocalChat l -> JCInfoLocal l
|
||||
ContactRequest g -> JCInfoContactRequest g
|
||||
ContactConnection c -> JCInfoContactConnection c
|
||||
|
@ -164,7 +137,7 @@ deriving instance Show AChatInfo
|
|||
jsonAChatInfo :: JSONChatInfo -> AChatInfo
|
||||
jsonAChatInfo = \case
|
||||
JCInfoDirect c -> AChatInfo SCTDirect $ DirectChat c
|
||||
JCInfoGroup g s -> AChatInfo SCTGroup $ GroupChat g s
|
||||
JCInfoGroup g -> AChatInfo SCTGroup $ GroupChat g
|
||||
JCInfoLocal l -> AChatInfo SCTLocal $ LocalChat l
|
||||
JCInfoContactRequest g -> AChatInfo SCTContactRequest $ ContactRequest g
|
||||
JCInfoContactConnection c -> AChatInfo SCTContactConnection $ ContactConnection c
|
||||
|
@ -190,6 +163,8 @@ data ChatItem (c :: ChatType) (d :: MsgDirection) = ChatItem
|
|||
}
|
||||
deriving (Show)
|
||||
|
||||
data NotInHistory = NotInHistory
|
||||
|
||||
data CIMention = CIMention
|
||||
{ memberId :: MemberId,
|
||||
-- member record can be created later than the mention is received
|
||||
|
@ -292,8 +267,8 @@ ciReactionAllowed ChatItem {content} = isJust $ ciMsgContent content
|
|||
data ChatDirection (c :: ChatType) (d :: MsgDirection) where
|
||||
CDDirectSnd :: Contact -> ChatDirection 'CTDirect 'MDSnd
|
||||
CDDirectRcv :: Contact -> ChatDirection 'CTDirect 'MDRcv
|
||||
CDGroupSnd :: GroupInfo -> Maybe GroupChatScopeInfo -> ChatDirection 'CTGroup 'MDSnd
|
||||
CDGroupRcv :: GroupInfo -> Maybe GroupChatScopeInfo -> GroupMember -> ChatDirection 'CTGroup 'MDRcv
|
||||
CDGroupSnd :: GroupInfo -> ChatDirection 'CTGroup 'MDSnd
|
||||
CDGroupRcv :: GroupInfo -> GroupMember -> ChatDirection 'CTGroup 'MDRcv
|
||||
CDLocalSnd :: NoteFolder -> ChatDirection 'CTLocal 'MDSnd
|
||||
CDLocalRcv :: NoteFolder -> ChatDirection 'CTLocal 'MDRcv
|
||||
|
||||
|
@ -301,8 +276,8 @@ toCIDirection :: ChatDirection c d -> CIDirection c d
|
|||
toCIDirection = \case
|
||||
CDDirectSnd _ -> CIDirectSnd
|
||||
CDDirectRcv _ -> CIDirectRcv
|
||||
CDGroupSnd _ _ -> CIGroupSnd
|
||||
CDGroupRcv _ _ m -> CIGroupRcv m
|
||||
CDGroupSnd _ -> CIGroupSnd
|
||||
CDGroupRcv _ m -> CIGroupRcv m
|
||||
CDLocalSnd _ -> CILocalSnd
|
||||
CDLocalRcv _ -> CILocalRcv
|
||||
|
||||
|
@ -310,8 +285,8 @@ toChatInfo :: ChatDirection c d -> ChatInfo c
|
|||
toChatInfo = \case
|
||||
CDDirectSnd c -> DirectChat c
|
||||
CDDirectRcv c -> DirectChat c
|
||||
CDGroupSnd g s -> GroupChat g s
|
||||
CDGroupRcv g s _ -> GroupChat g s
|
||||
CDGroupSnd g -> GroupChat g
|
||||
CDGroupRcv g _ -> GroupChat g
|
||||
CDLocalSnd l -> LocalChat l
|
||||
CDLocalRcv l -> LocalChat l
|
||||
|
||||
|
@ -1399,8 +1374,6 @@ instance MsgDirectionI d => ToJSON (CIFile d) where
|
|||
toJSON = $(JQ.mkToJSON defaultJSON ''CIFile)
|
||||
toEncoding = $(JQ.mkToEncoding defaultJSON ''CIFile)
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GCS") ''GroupChatScope)
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCI") ''JSONCIDirection)
|
||||
|
||||
instance (ChatTypeI c, MsgDirectionI d) => FromJSON (CIDirection c d) where
|
||||
|
|
|
@ -206,8 +206,6 @@ ciRequiresAttention content = case msgDirection @d of
|
|||
CIRcvGroupEvent rge -> case rge of
|
||||
RGEMemberAdded {} -> False
|
||||
RGEMemberConnected -> False
|
||||
RGEMemberAccepted {} -> False
|
||||
RGEUserAccepted -> False
|
||||
RGEMemberLeft -> False
|
||||
RGEMemberRole {} -> False
|
||||
RGEMemberBlocked {} -> False
|
||||
|
@ -219,7 +217,6 @@ ciRequiresAttention content = case msgDirection @d of
|
|||
RGEInvitedViaGroupLink -> False
|
||||
RGEMemberCreatedContact -> False
|
||||
RGEMemberProfileUpdated {} -> False
|
||||
RGENewMemberPendingReview -> True
|
||||
CIRcvConnEvent _ -> True
|
||||
CIRcvChatFeature {} -> False
|
||||
CIRcvChatPreference {} -> False
|
||||
|
@ -320,8 +317,6 @@ rcvGroupEventToText :: RcvGroupEvent -> Text
|
|||
rcvGroupEventToText = \case
|
||||
RGEMemberAdded _ p -> "added " <> profileToText p
|
||||
RGEMemberConnected -> "connected"
|
||||
RGEMemberAccepted _ p -> "accepted " <> profileToText p
|
||||
RGEUserAccepted -> "accepted you"
|
||||
RGEMemberLeft -> "left"
|
||||
RGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> safeDecodeUtf8 (strEncode r)
|
||||
RGEMemberBlocked _ p blocked -> (if blocked then "blocked" else "unblocked") <> " " <> profileToText p
|
||||
|
@ -333,7 +328,6 @@ rcvGroupEventToText = \case
|
|||
RGEInvitedViaGroupLink -> "invited via your group link"
|
||||
RGEMemberCreatedContact -> "started direct connection with you"
|
||||
RGEMemberProfileUpdated {} -> "updated profile"
|
||||
RGENewMemberPendingReview -> "new member wants to join the group"
|
||||
|
||||
sndGroupEventToText :: SndGroupEvent -> Text
|
||||
sndGroupEventToText = \case
|
||||
|
@ -343,18 +337,6 @@ sndGroupEventToText = \case
|
|||
SGEMemberDeleted _ p -> "removed " <> profileToText p
|
||||
SGEUserLeft -> "left"
|
||||
SGEGroupUpdated _ -> "group profile updated"
|
||||
SGEMemberAccepted _ _p -> "you accepted this member"
|
||||
SGEUserPendingReview -> "please wait for group moderators to review your request to join the group"
|
||||
|
||||
-- used to send to members with old version
|
||||
pendingReviewMessage :: Text
|
||||
pendingReviewMessage =
|
||||
"Please wait for group moderators to review your request to join the group."
|
||||
|
||||
-- used to send to members with old version
|
||||
acceptedToGroupMessage :: Text
|
||||
acceptedToGroupMessage =
|
||||
"You are accepted to the group."
|
||||
|
||||
rcvConnEventToText :: RcvConnEvent -> Text
|
||||
rcvConnEventToText = \case
|
||||
|
|
|
@ -14,9 +14,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption)
|
|||
|
||||
data RcvGroupEvent
|
||||
= RGEMemberAdded {groupMemberId :: GroupMemberId, profile :: Profile} -- CRJoinedGroupMemberConnecting
|
||||
| RGEMemberConnected -- CEvtUserJoinedGroup, CRJoinedGroupMember, CEvtConnectedToGroupMember
|
||||
| RGEMemberAccepted {groupMemberId :: GroupMemberId, profile :: Profile}
|
||||
| RGEUserAccepted
|
||||
| RGEMemberConnected -- CRUserJoinedGroup, CRJoinedGroupMember, CRConnectedToGroupMember
|
||||
| RGEMemberLeft -- CRLeftMember
|
||||
| RGEMemberRole {groupMemberId :: GroupMemberId, profile :: Profile, role :: GroupMemberRole}
|
||||
| RGEMemberBlocked {groupMemberId :: GroupMemberId, profile :: Profile, blocked :: Bool} -- CRMemberBlockedForAll
|
||||
|
@ -31,7 +29,6 @@ data RcvGroupEvent
|
|||
| RGEInvitedViaGroupLink -- CRSentGroupInvitationViaLink
|
||||
| RGEMemberCreatedContact -- CRNewMemberContactReceivedInv
|
||||
| RGEMemberProfileUpdated {fromProfile :: Profile, toProfile :: Profile} -- CRGroupMemberUpdated
|
||||
| RGENewMemberPendingReview
|
||||
deriving (Show)
|
||||
|
||||
data SndGroupEvent
|
||||
|
@ -41,8 +38,6 @@ data SndGroupEvent
|
|||
| SGEMemberDeleted {groupMemberId :: GroupMemberId, profile :: Profile} -- CRUserDeletedMembers
|
||||
| SGEUserLeft -- CRLeftMemberUser
|
||||
| SGEGroupUpdated {groupProfile :: GroupProfile} -- CRGroupUpdated
|
||||
| SGEMemberAccepted {groupMemberId :: GroupMemberId, profile :: Profile}
|
||||
| SGEUserPendingReview
|
||||
deriving (Show)
|
||||
|
||||
data RcvConnEvent
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue