diff --git a/README.md b/README.md
index 554c6068d9..40d552b84d 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
# SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design!
-[
](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) [
](https://www.privacyguides.org/en/real-time-communication/#simplex-chat) [
](https://www.whonix.org/wiki/Chat#Recommendation) [
](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/)
+[
](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) [
](https://www.privacyguides.org/en/real-time-communication/#simplex-chat) [
](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/)
## Welcome to SimpleX Chat!
@@ -110,15 +110,6 @@ 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.
@@ -150,6 +141,15 @@ 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!
diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift
index 3bf4cb7b56..2ddaf1d2af 100644
--- a/apps/ios/Shared/Model/AppAPITypes.swift
+++ b/apps/ios/Shared/Model/AppAPITypes.swift
@@ -39,9 +39,9 @@ enum ChatCommand: ChatCmdProtocol {
case apiGetSettings(settings: AppSettings)
case apiGetChatTags(userId: Int64)
case apiGetChats(userId: Int64)
- case apiGetChat(chatId: ChatId, pagination: ChatPagination, search: String)
- case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64)
- case apiSendMessages(type: ChatType, id: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
+ case apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag?, pagination: ChatPagination, search: String)
+ case apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64)
+ case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
case apiCreateChatTag(tag: ChatTagData)
case apiSetChatTags(type: ChatType, id: Int64, tagIds: [Int64])
case apiDeleteChatTag(tagId: Int64)
@@ -49,15 +49,15 @@ enum ChatCommand: ChatCmdProtocol {
case apiReorderChatTags(tagIds: [Int64])
case apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage])
case apiReportMessage(groupId: Int64, chatItemId: Int64, reportReason: ReportReason, reportText: String)
- case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool)
- case apiDeleteChatItem(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode)
+ case apiUpdateChatItem(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64, updatedMessage: UpdatedMessage, live: Bool)
+ case apiDeleteChatItem(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64], mode: CIDeleteMode)
case apiDeleteMemberChatItem(groupId: Int64, itemIds: [Int64])
case apiArchiveReceivedReports(groupId: Int64)
case apiDeleteReceivedReports(groupId: Int64, itemIds: [Int64], mode: CIDeleteMode)
- case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction)
+ case apiChatItemReaction(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64, add: Bool, reaction: MsgReaction)
case apiGetReactionMembers(userId: Int64, groupId: Int64, itemId: Int64, reaction: MsgReaction)
- case apiPlanForwardChatItems(toChatType: ChatType, toChatId: Int64, itemIds: [Int64])
- case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?)
+ case apiPlanForwardChatItems(fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64])
+ case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64], ttl: Int?)
case apiGetNtfToken
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
case apiVerifyToken(token: DeviceToken, nonce: String, code: String)
@@ -68,6 +68,8 @@ 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 apiDeleteMemberSupportChat(groupId: Int64, groupMemberId: Int64)
case apiMembersRole(groupId: Int64, memberIds: [Int64], memberRole: GroupMemberRole)
case apiBlockMembersForAll(groupId: Int64, memberIds: [Int64], blocked: Bool)
case apiRemoveMembers(groupId: Int64, memberIds: [Int64], withMessages: Bool)
@@ -147,8 +149,8 @@ enum ChatCommand: ChatCmdProtocol {
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
// WebRTC calls /
case apiGetNetworkStatuses
- case apiChatRead(type: ChatType, id: Int64)
- case apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64])
+ case apiChatRead(type: ChatType, id: Int64, scope: GroupChatScope?)
+ case apiChatItemsRead(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64])
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
case receiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?, inline: Bool?)
case setFileToReceive(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?)
@@ -209,15 +211,16 @@ enum ChatCommand: ChatCmdProtocol {
case let .apiGetSettings(settings): return "/_get app settings \(encodeJSON(settings))"
case let .apiGetChatTags(userId): return "/_get tags \(userId)"
case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on"
- case let .apiGetChat(chatId, pagination, search): return "/_get chat \(chatId) \(pagination.cmdString)" +
- (search == "" ? "" : " search=\(search)")
- case let .apiGetChatItemInfo(type, id, itemId): return "/_get item info \(ref(type, id)) \(itemId)"
- case let .apiSendMessages(type, id, live, ttl, composedMessages):
+ case let .apiGetChat(chatId, scope, contentTag, pagination, search):
+ let tag = contentTag != nil ? " content=\(contentTag!.rawValue)" : ""
+ return "/_get chat \(chatId)\(scopeRef(scope: scope))\(tag) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)")
+ case let .apiGetChatItemInfo(type, id, scope, itemId): return "/_get item info \(ref(type, id, scope: scope)) \(itemId)"
+ case let .apiSendMessages(type, id, scope, live, ttl, composedMessages):
let msgs = encodeJSON(composedMessages)
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
- return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
+ return "/_send \(ref(type, id, scope: scope)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
case let .apiCreateChatTag(tag): return "/_create tag \(encodeJSON(tag))"
- case let .apiSetChatTags(type, id, tagIds): return "/_tags \(ref(type, id)) \(tagIds.map({ "\($0)" }).joined(separator: ","))"
+ case let .apiSetChatTags(type, id, tagIds): return "/_tags \(ref(type, id, scope: nil)) \(tagIds.map({ "\($0)" }).joined(separator: ","))"
case let .apiDeleteChatTag(tagId): return "/_delete tag \(tagId)"
case let .apiUpdateChatTag(tagId, tagData): return "/_update tag \(tagId) \(encodeJSON(tagData))"
case let .apiReorderChatTags(tagIds): return "/_reorder tags \(tagIds.map({ "\($0)" }).joined(separator: ","))"
@@ -226,17 +229,17 @@ enum ChatCommand: ChatCmdProtocol {
return "/_create *\(noteFolderId) json \(msgs)"
case let .apiReportMessage(groupId, chatItemId, reportReason, reportText):
return "/_report #\(groupId) \(chatItemId) reason=\(reportReason) \(reportText)"
- case let .apiUpdateChatItem(type, id, itemId, um, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(um.cmdString)"
- case let .apiDeleteChatItem(type, id, itemIds, mode): return "/_delete item \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)"
+ case let .apiUpdateChatItem(type, id, scope, itemId, um, live): return "/_update item \(ref(type, id, scope: scope)) \(itemId) live=\(onOff(live)) \(um.cmdString)"
+ case let .apiDeleteChatItem(type, id, scope, itemIds, mode): return "/_delete item \(ref(type, id, scope: scope)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)"
case let .apiDeleteMemberChatItem(groupId, itemIds): return "/_delete member item #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ","))"
case let .apiArchiveReceivedReports(groupId): return "/_archive reports #\(groupId)"
case let .apiDeleteReceivedReports(groupId, itemIds, mode): return "/_delete reports #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)"
- case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))"
+ case let .apiChatItemReaction(type, id, scope, itemId, add, reaction): return "/_reaction \(ref(type, id, scope: scope)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))"
case let .apiGetReactionMembers(userId, groupId, itemId, reaction): return "/_reaction members \(userId) #\(groupId) \(itemId) \(encodeJSON(reaction))"
- case let .apiPlanForwardChatItems(type, id, itemIds): return "/_forward plan \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ","))"
- case let .apiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl):
+ case let .apiPlanForwardChatItems(type, id, scope, itemIds): return "/_forward plan \(ref(type, id, scope: scope)) \(itemIds.map({ "\($0)" }).joined(separator: ","))"
+ case let .apiForwardChatItems(toChatType, toChatId, toScope, fromChatType, fromChatId, fromScope, itemIds, ttl):
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
- return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) ttl=\(ttlStr)"
+ return "/_forward \(ref(toChatType, toChatId, scope: toScope)) \(ref(fromChatType, fromChatId, scope: fromScope)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) ttl=\(ttlStr)"
case .apiGetNtfToken: return "/_ntf get "
case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)"
case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)"
@@ -247,6 +250,8 @@ 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 .apiDeleteMemberSupportChat(groupId, groupMemberId): return "/_delete member chat #\(groupId) \(groupMemberId)"
case let .apiMembersRole(groupId, memberIds, memberRole): return "/_member role #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) \(memberRole.rawValue)"
case let .apiBlockMembersForAll(groupId, memberIds, blocked): return "/_block #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) blocked=\(onOff(blocked))"
case let .apiRemoveMembers(groupId, memberIds, withMessages): return "/_remove #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) messages=\(onOff(withMessages))"
@@ -270,13 +275,13 @@ enum ChatCommand: ChatCmdProtocol {
case let .apiAcceptConditions(conditionsId, operatorIds): return "/_accept_conditions \(conditionsId) \(joinedIds(operatorIds))"
case let .apiSetChatItemTTL(userId, seconds): return "/_ttl \(userId) \(chatItemTTLStr(seconds: seconds))"
case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)"
- case let .apiSetChatTTL(userId, type, id, seconds): return "/_ttl \(userId) \(ref(type, id)) \(chatItemTTLStr(seconds: seconds))"
+ case let .apiSetChatTTL(userId, type, id, seconds): return "/_ttl \(userId) \(ref(type, id, scope: nil)) \(chatItemTTLStr(seconds: seconds))"
case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))"
case .apiGetNetworkConfig: return "/network"
case let .apiSetNetworkInfo(networkInfo): return "/_network info \(encodeJSON(networkInfo))"
case .reconnectAllServers: return "/reconnect"
case let .reconnectServer(userId, smpServer): return "/reconnect \(userId) \(smpServer)"
- case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))"
+ case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id, scope: nil)) \(encodeJSON(chatSettings))"
case let .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))"
case let .apiContactInfo(contactId): return "/_info @\(contactId)"
case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)"
@@ -308,8 +313,8 @@ enum ChatCommand: ChatCmdProtocol {
case let .apiConnectPlan(userId, connLink): return "/_connect plan \(userId) \(connLink)"
case let .apiConnect(userId, incognito, connLink): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connLink.connFullLink) \(connLink.connShortLink ?? "")"
case let .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)"
- case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id)) \(chatDeleteMode.cmdString)"
- case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
+ case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id, scope: nil)) \(chatDeleteMode.cmdString)"
+ case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id, scope: nil))"
case let .apiListContacts(userId): return "/_contacts \(userId)"
case let .apiUpdateProfile(userId, profile): return "/_profile \(userId) \(encodeJSON(profile))"
case let .apiSetContactPrefs(contactId, preferences): return "/_set prefs @\(contactId) \(encodeJSON(preferences))"
@@ -334,9 +339,9 @@ enum ChatCommand: ChatCmdProtocol {
case .apiGetCallInvitations: return "/_call get"
case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)"
case .apiGetNetworkStatuses: return "/_network_statuses"
- case let .apiChatRead(type, id): return "/_read chat \(ref(type, id))"
- case let .apiChatItemsRead(type, id, itemIds): return "/_read chat items \(ref(type, id)) \(joinedIds(itemIds))"
- case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
+ case let .apiChatRead(type, id, scope): return "/_read chat \(ref(type, id, scope: scope))"
+ case let .apiChatItemsRead(type, id, scope, itemIds): return "/_read chat items \(ref(type, id, scope: scope)) \(joinedIds(itemIds))"
+ case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id, scope: nil)) \(onOff(unreadChat))"
case let .receiveFile(fileId, userApprovedRelays, encrypt, inline): return "/freceive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))"
case let .setFileToReceive(fileId, userApprovedRelays, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))"
case let .cancelFile(fileId): return "/fcancel \(fileId)"
@@ -421,6 +426,8 @@ enum ChatCommand: ChatCmdProtocol {
case .apiNewGroup: return "apiNewGroup"
case .apiAddMember: return "apiAddMember"
case .apiJoinGroup: return "apiJoinGroup"
+ case .apiAcceptMember: return "apiAcceptMember"
+ case .apiDeleteMemberSupportChat: return "apiDeleteMemberSupportChat"
case .apiMembersRole: return "apiMembersRole"
case .apiBlockMembersForAll: return "apiBlockMembersForAll"
case .apiRemoveMembers: return "apiRemoveMembers"
@@ -523,8 +530,20 @@ enum ChatCommand: ChatCmdProtocol {
}
}
- func ref(_ type: ChatType, _ id: Int64) -> String {
- "\(type.rawValue)\(id)"
+ func ref(_ type: ChatType, _ id: Int64, scope: GroupChatScope?) -> String {
+ "\(type.rawValue)\(id)\(scopeRef(scope: scope))"
+ }
+
+ func scopeRef(scope: GroupChatScope?) -> String {
+ switch (scope) {
+ case .none: ""
+ case let .memberSupport(groupMemberId_):
+ if let groupMemberId = groupMemberId_ {
+ "(_support:\(groupMemberId))"
+ } else {
+ "(_support)"
+ }
+ }
}
func joinedIds(_ ids: [Int64]) -> String {
@@ -711,6 +730,7 @@ 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)
@@ -749,6 +769,7 @@ 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"
@@ -781,6 +802,7 @@ 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))
@@ -831,6 +853,8 @@ 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 memberSupportChatDeleted(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)
@@ -879,6 +903,8 @@ enum ChatResponse2: Decodable, ChatAPIResult {
case .userDeletedMembers: "userDeletedMembers"
case .leftMemberUser: "leftMemberUser"
case .groupMembers: "groupMembers"
+ case .memberAccepted: "memberAccepted"
+ case .memberSupportChatDeleted: "memberSupportChatDeleted"
case .membersRoleUser: "membersRoleUser"
case .membersBlockedForAllUser: "membersBlockedForAllUser"
case .groupUpdated: "groupUpdated"
@@ -923,6 +949,8 @@ 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 .memberSupportChatDeleted(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))
@@ -988,6 +1016,7 @@ 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)
@@ -1064,6 +1093,7 @@ 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"
@@ -1144,6 +1174,7 @@ 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)")
diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift
index 9b9fda0397..f8cb022095 100644
--- a/apps/ios/Shared/Model/ChatModel.swift
+++ b/apps/ios/Shared/Model/ChatModel.swift
@@ -52,8 +52,26 @@ private func addTermItem(_ items: inout [TerminalItem], _ item: TerminalItem) {
items.append(item)
}
+// analogue for SecondaryContextFilter in Kotlin
+enum SecondaryItemsModelFilter {
+ case groupChatScopeContext(groupScopeInfo: GroupChatScopeInfo)
+ case msgContentTagContext(contentTag: MsgContentTag)
+
+ func descr() -> String {
+ switch self {
+ case let .groupChatScopeContext(groupScopeInfo):
+ return "groupChatScopeContext \(groupScopeInfo.toChatScope())"
+ case let .msgContentTagContext(contentTag):
+ return "msgContentTagContext \(contentTag.rawValue)"
+ }
+ }
+}
+
+// analogue for ChatsContext in Kotlin
class ItemsModel: ObservableObject {
- static let shared = ItemsModel()
+ static let shared = ItemsModel(secondaryIMFilter: nil)
+ public var secondaryIMFilter: SecondaryItemsModelFilter?
+ public var preloadState = PreloadState()
private let publisher = ObservableObjectPublisher()
private var bag = Set()
var reversedChatItems: [ChatItem] = [] {
@@ -77,13 +95,20 @@ class ItemsModel: ObservableObject {
chatState.splits.isEmpty || chatState.splits.first != reversedChatItems.first?.id
}
- init() {
+ init(secondaryIMFilter: SecondaryItemsModelFilter? = nil) {
+ self.secondaryIMFilter = secondaryIMFilter
publisher
.throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true)
.sink { self.objectWillChange.send() }
.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()
@@ -99,7 +124,7 @@ class ItemsModel: ObservableObject {
loadChatTask = Task {
await MainActor.run { self.isLoading = true }
// try? await Task.sleep(nanoseconds: 1000_000000)
- await loadChat(chatId: chatId)
+ await loadChat(chatId: chatId, im: self)
if !Task.isCancelled {
await MainActor.run {
self.isLoading = false
@@ -114,7 +139,7 @@ class ItemsModel: ObservableObject {
loadChatTask?.cancel()
loadChatTask = Task {
// try? await Task.sleep(nanoseconds: 1000_000000)
- await loadChat(chatId: chatId, openAroundItemId: openAroundItemId, clearItems: openAroundItemId == nil)
+ await loadChat(chatId: chatId, im: self, openAroundItemId: openAroundItemId, clearItems: openAroundItemId == nil)
if !Task.isCancelled {
await MainActor.run {
if openAroundItemId == nil {
@@ -124,6 +149,34 @@ class ItemsModel: ObservableObject {
}
}
}
+
+ public var contentTag: MsgContentTag? {
+ switch secondaryIMFilter {
+ case nil: nil
+ case .groupChatScopeContext: nil
+ case let .msgContentTagContext(contentTag): contentTag
+ }
+ }
+
+ public var groupScopeInfo: GroupChatScopeInfo? {
+ switch secondaryIMFilter {
+ case nil: nil
+ case let .groupChatScopeContext(scopeInfo): scopeInfo
+ case .msgContentTagContext: nil
+ }
+ }
+}
+
+class PreloadState {
+ var prevFirstVisible: Int64 = Int64.min
+ var prevItemsCount: Int = 0
+ var preloading: Bool = false
+
+ func clear() {
+ prevFirstVisible = Int64.min
+ prevItemsCount = 0
+ preloading = false
+ }
}
class ChatTagsModel: ObservableObject {
@@ -287,7 +340,6 @@ final class ChatModel: ObservableObject {
// current chat
@Published var chatId: String?
@Published var openAroundItemId: ChatItem.ID? = nil
- var chatItemStatuses: Dictionary = [:]
@Published var chatToTop: String?
@Published var groupMembers: [GMember] = []
@Published var groupMembersIndexes: Dictionary = [:] // groupMemberId to index in groupMembers list
@@ -336,6 +388,10 @@ 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
@@ -393,7 +449,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
@@ -446,7 +502,11 @@ final class ChatModel: ObservableObject {
func updateChatInfo(_ cInfo: ChatInfo) {
if let i = getChatIndex(cInfo.id) {
- chats[i].chatInfo = cInfo
+ if case let .group(groupInfo, groupChatScope) = cInfo, groupChatScope != nil {
+ chats[i].chatInfo = .group(groupInfo: groupInfo, groupChatScope: nil)
+ } else {
+ chats[i].chatInfo = cInfo
+ }
chats[i].created = Date.now
}
}
@@ -468,7 +528,7 @@ final class ChatModel: ObservableObject {
}
func updateGroup(_ groupInfo: GroupInfo) {
- updateChat(.group(groupInfo: groupInfo))
+ updateChat(.group(groupInfo: groupInfo, groupChatScope: nil))
}
private func updateChat(_ cInfo: ChatInfo, addMissing: Bool = true) {
@@ -511,77 +571,112 @@ 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 previews
+ // update chat list
if let i = getChatIndex(cInfo.id) {
- chats[i].chatItems = switch cInfo {
- case .group:
- if let currentPreviewItem = chats[i].chatItems.first {
- if cItem.meta.itemTs >= currentPreviewItem.meta.itemTs {
- [cItem]
+ // update 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 {
- [currentPreviewItem]
+ [cItem]
}
- } else {
+ default:
[cItem]
}
- default:
- [cItem]
- }
- if case .rcvNew = cItem.meta.itemStatus {
- unreadCollector.changeUnreadCounter(cInfo.id, by: 1, unreadMentions: cItem.meta.userMention ? 1 : 0)
+ 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 {
- addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
+ if cInfo.groupChatScope() == nil {
+ addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
+ } else {
+ addChat(Chat(chatInfo: cInfo, chatItems: []))
+ }
}
- // add to current chat
- if chatId == cInfo.id {
- _ = _upsertChatItem(cInfo, cItem)
+ // add to current scope
+ if let ciIM = getCIItemsModel(cInfo, cItem) {
+ _ = _upsertChatItem(ciIM, cInfo, cItem)
+ }
+ }
+
+ func getCIItemsModel(_ cInfo: ChatInfo, _ ci: ChatItem) -> ItemsModel? {
+ let cInfoScope = cInfo.groupChatScope()
+ if let cInfoScope = cInfoScope {
+ switch cInfoScope {
+ case .memberSupport:
+ switch secondaryIM?.secondaryIMFilter {
+ case .none:
+ return nil
+ case let .groupChatScopeContext(groupScopeInfo):
+ return (cInfo.id == chatId && sameChatScope(cInfoScope, groupScopeInfo.toChatScope())) ? secondaryIM : nil
+ case let .msgContentTagContext(contentTag):
+ return (cInfo.id == chatId && ci.isReport && contentTag == .report) ? secondaryIM : nil
+ }
+ }
+ } else {
+ return cInfo.id == chatId ? im : nil
}
}
func upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
- // update previews
- var res: Bool
- if let chat = getChat(cInfo.id) {
- if let pItem = chat.chatItems.last {
- if pItem.id == cItem.id || (chatId == cInfo.id && im.reversedChatItems.first(where: { $0.id == cItem.id }) == nil) {
+ // update chat list
+ var itemAdded: Bool = false
+ if cInfo.groupChatScope() == nil {
+ if let chat = getChat(cInfo.id) {
+ if let pItem = chat.chatItems.last {
+ if pItem.id == cItem.id || (chatId == cInfo.id && im.reversedChatItems.first(where: { $0.id == cItem.id }) == nil) {
+ chat.chatItems = [cItem]
+ }
+ } else {
chat.chatItems = [cItem]
}
} else {
- chat.chatItems = [cItem]
+ addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
+ itemAdded = true
+ }
+ if cItem.isDeletedContent || cItem.meta.itemDeleted != nil {
+ VoiceItemState.stopVoiceInChatView(cInfo, cItem)
}
- res = false
- } else {
- addChat(Chat(chatInfo: cInfo, chatItems: [cItem]))
- res = true
}
- if cItem.isDeletedContent || cItem.meta.itemDeleted != nil {
- VoiceItemState.stopVoiceInChatView(cInfo, cItem)
+ // update current scope
+ if let ciIM = getCIItemsModel(cInfo, cItem) {
+ itemAdded = _upsertChatItem(ciIM, cInfo, cItem)
}
- // update current chat
- return chatId == cInfo.id ? _upsertChatItem(cInfo, cItem) : res
+ return itemAdded
}
- private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
- if let i = getChatItemIndex(cItem) {
- _updateChatItem(at: i, with: cItem)
- ChatItemDummyModel.shared.sendUpdate()
+ private func _upsertChatItem(_ ciIM: ItemsModel, _ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
+ if let i = getChatItemIndex(ciIM, cItem) {
+ let oldStatus = ciIM.reversedChatItems[i].meta.itemStatus
+ let newStatus = cItem.meta.itemStatus
+ var ci = cItem
+ if shouldKeepOldSndCIStatus(oldStatus: oldStatus, newStatus: newStatus) {
+ ci.meta.itemStatus = oldStatus
+ }
+ _updateChatItem(ciIM: ciIM, at: i, with: ci)
+ ChatItemDummyModel.shared.sendUpdate() // TODO [knocking] review what's this
return false
} else {
- var ci = cItem
- if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus {
- ci.meta.itemStatus = status
- }
- im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0)
- im.chatState.itemAdded((ci.id, ci.isRcvNew), hasLiveDummy ? 1 : 0)
- im.itemAdded = true
+ ciIM.reversedChatItems.insert(cItem, at: hasLiveDummy ? 1 : 0)
+ ciIM.chatState.itemAdded((cItem.id, cItem.isRcvNew), hasLiveDummy ? 1 : 0)
+ ciIM.itemAdded = true
ChatItemDummyModel.shared.sendUpdate()
return true
}
@@ -595,40 +690,42 @@ final class ChatModel: ObservableObject {
}
func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem, status: CIStatus? = nil) {
- if chatId == cInfo.id, let i = getChatItemIndex(cItem) {
+ if let ciIM = getCIItemsModel(cInfo, cItem),
+ let i = getChatItemIndex(ciIM, cItem) {
withConditionalAnimation {
- _updateChatItem(at: i, with: cItem)
+ _updateChatItem(ciIM: ciIM, at: i, with: cItem)
}
- } else if let status = status {
- chatItemStatuses.updateValue(status, forKey: cItem.id)
}
}
- private func _updateChatItem(at i: Int, with cItem: ChatItem) {
- im.reversedChatItems[i] = cItem
- im.reversedChatItems[i].viewTimestamp = .now
+ private func _updateChatItem(ciIM: ItemsModel, at i: Int, with cItem: ChatItem) {
+ ciIM.reversedChatItems[i] = cItem
+ ciIM.reversedChatItems[i].viewTimestamp = .now
}
- func getChatItemIndex(_ cItem: ChatItem) -> Int? {
- im.reversedChatItems.firstIndex(where: { $0.id == cItem.id })
+ func getChatItemIndex(_ ciIM: ItemsModel, _ cItem: ChatItem) -> Int? {
+ ciIM.reversedChatItems.firstIndex(where: { $0.id == cItem.id })
}
func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
- if cItem.isRcvNew {
- unreadCollector.changeUnreadCounter(cInfo.id, by: -1, unreadMentions: cItem.meta.userMention ? -1 : 0)
- }
- // update previews
- if let chat = getChat(cInfo.id) {
- if let pItem = chat.chatItems.last, pItem.id == cItem.id {
- chat.chatItems = [ChatItem.deletedItemDummy()]
+ // update chat list
+ if cInfo.groupChatScope() == nil {
+ if cItem.isRcvNew {
+ unreadCollector.changeUnreadCounter(cInfo.id, by: -1, unreadMentions: cItem.meta.userMention ? -1 : 0)
+ }
+ // update previews
+ if let chat = getChat(cInfo.id) {
+ if let pItem = chat.chatItems.last, pItem.id == cItem.id {
+ chat.chatItems = [ChatItem.deletedItemDummy()]
+ }
}
}
- // remove from current chat
- if chatId == cInfo.id {
- if let i = getChatItemIndex(cItem) {
+ // remove from current scope
+ if let ciIM = getCIItemsModel(cInfo, cItem) {
+ if let i = getChatItemIndex(ciIM, cItem) {
withAnimation {
- let item = im.reversedChatItems.remove(at: i)
- im.chatState.itemsRemoved([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed())
+ let item = ciIM.reversedChatItems.remove(at: i)
+ ciIM.chatState.itemsRemoved([(item.id, i, item.isRcvNew)], im.reversedChatItems.reversed())
}
}
}
@@ -644,7 +741,7 @@ final class ChatModel: ObservableObject {
if chatId == groupInfo.id {
for i in 0.. = []
var i = 0
var ids = Set(itemIds)
- while i < im.reversedChatItems.count && !ids.isEmpty {
- let item = im.reversedChatItems[i]
+ while i < chatIM.reversedChatItems.count && !ids.isEmpty {
+ let item = chatIM.reversedChatItems[i]
if ids.contains(item.id) && item.isRcvNew {
- markChatItemRead_(i)
+ markChatItemRead_(chatIM, i)
unreadItemIds.insert(item.id)
ids.remove(item.id)
}
i += 1
}
- im.chatState.itemsRead(unreadItemIds, im.reversedChatItems.reversed())
+ chatIM.chatState.itemsRead(unreadItemIds, chatIM.reversedChatItems.reversed())
}
self.unreadCollector.changeUnreadCounter(cInfo.id, by: -itemIds.count, unreadMentions: -mentionsRead)
}
@@ -888,13 +984,13 @@ final class ChatModel: ObservableObject {
}
}
- private func markChatItemRead_(_ i: Int) {
- let meta = im.reversedChatItems[i].meta
+ private func markChatItemRead_(_ chatIM: ItemsModel, _ i: Int) {
+ let meta = chatIM.reversedChatItems[i].meta
if case .rcvNew = meta.itemStatus {
- im.reversedChatItems[i].meta.itemStatus = .rcvRead
- im.reversedChatItems[i].viewTimestamp = .now
+ chatIM.reversedChatItems[i].meta.itemStatus = .rcvRead
+ chatIM.reversedChatItems[i].viewTimestamp = .now
if meta.itemLive != true, let ttl = meta.itemTimed?.ttl {
- im.reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl)
+ chatIM.reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl)
}
}
}
@@ -973,7 +1069,7 @@ final class ChatModel: ObservableObject {
var count = 0
var ns: [String] = []
if let ciCategory = chatItem.mergeCategory,
- var i = getChatItemIndex(chatItem) {
+ var i = getChatItemIndex(im, chatItem) { // TODO [knocking] review: use getCIItemsModel?
while i < im.reversedChatItems.count {
let ci = im.reversedChatItems[i]
if ci.mergeCategory != ciCategory { break }
@@ -989,7 +1085,7 @@ final class ChatModel: ObservableObject {
// returns the index of the passed item and the next item (it has smaller index)
func getNextChatItem(_ ci: ChatItem) -> (Int?, ChatItem?) {
- if let i = getChatItemIndex(ci) {
+ if let i = getChatItemIndex(im, ci) { // TODO [knocking] review: use getCIItemsModel?
(i, i > 0 ? im.reversedChatItems[i - 1] : nil)
} else {
(nil, nil)
@@ -1100,7 +1196,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)
}
}
@@ -1164,6 +1260,18 @@ 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: [])
}
diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift
index d92411decd..6b938aaa4d 100644
--- a/apps/ios/Shared/Model/SimpleXAPI.swift
+++ b/apps/ios/Shared/Model/SimpleXAPI.swift
@@ -344,43 +344,54 @@ func apiGetChatTagsAsync() async throws -> [ChatTag] {
let loadItemsPerPage = 50
-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))
+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))
if case let .apiChat(_, chat, navInfo) = r { return (Chat.init(chat), navInfo ?? NavigationInfo()) }
throw r.unexpected
}
-func loadChat(chat: Chat, search: String = "", clearItems: Bool = true) async {
- await loadChat(chatId: chat.chatInfo.id, search: search, clearItems: clearItems)
+func loadChat(chat: Chat, im: ItemsModel, search: String = "", clearItems: Bool = true) async {
+ await loadChat(chatId: chat.chatInfo.id, im: im, search: search, clearItems: clearItems)
}
-func loadChat(chatId: ChatId, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async {
- let m = ChatModel.shared
- let im = ItemsModel.shared
+func loadChat(chatId: ChatId, im: ItemsModel, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async {
await MainActor.run {
- m.chatItemStatuses = [:]
if clearItems {
im.reversedChatItems = []
- ItemsModel.shared.chatState.clear()
+ im.chatState.clear()
}
}
- await apiLoadMessages(chatId, openAroundItemId != nil ? .around(chatItemId: openAroundItemId!, count: loadItemsPerPage) : (search == "" ? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage)), im.chatState, search, openAroundItemId, { 0...0 })
+ await apiLoadMessages(
+ chatId,
+ im,
+ ( // pagination
+ openAroundItemId != nil
+ ? .around(chatItemId: openAroundItemId!, count: loadItemsPerPage)
+ : (
+ search == ""
+ ? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage)
+ )
+ ),
+ search,
+ openAroundItemId,
+ { 0...0 }
+ )
}
-func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -> ChatItemInfo {
- let r: ChatResponse0 = try await chatSendCmd(.apiGetChatItemInfo(type: type, id: id, itemId: itemId))
+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))
if case let .chatItemInfo(_, _, chatItemInfo) = r { return chatItemInfo }
throw r.unexpected
}
-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))
+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))
if case let .forwardPlan(_, chatItemIds, forwardConfimation) = r { return (chatItemIds, forwardConfimation) }
throw r.unexpected
}
-func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? {
- let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemIds: itemIds, ttl: ttl)
+func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, toScope: GroupChatScope?, fromChatType: ChatType, fromChatId: Int64, fromScope: GroupChatScope?, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? {
+ let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, toScope: toScope, fromChatType: fromChatType, fromChatId: fromChatId, fromScope: fromScope, itemIds: itemIds, ttl: ttl)
return await processSendMessageCmd(toChatType: toChatType, cmd: cmd)
}
@@ -412,8 +423,8 @@ func apiReorderChatTags(tagIds: [Int64]) async throws {
try await sendCommandOkResp(.apiReorderChatTags(tagIds: tagIds))
}
-func apiSendMessages(type: ChatType, id: Int64, live: Bool = false, ttl: Int? = nil, composedMessages: [ComposedMessage]) async -> [ChatItem]? {
- let cmd: ChatCommand = .apiSendMessages(type: type, id: id, live: live, ttl: ttl, composedMessages: composedMessages)
+func apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, live: Bool = false, ttl: Int? = nil, composedMessages: [ComposedMessage]) async -> [ChatItem]? {
+ let cmd: ChatCommand = .apiSendMessages(type: type, id: id, scope: scope, live: live, ttl: ttl, composedMessages: composedMessages)
return await processSendMessageCmd(toChatType: type, cmd: cmd)
}
@@ -490,8 +501,8 @@ private func createChatItemsErrorAlert(_ r: ChatError) {
)
}
-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)
+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)
switch r {
case let .chatItemUpdated(_, aChatItem): return aChatItem.chatItem
case let .chatItemNotChanged(_, aChatItem): return aChatItem.chatItem
@@ -499,8 +510,8 @@ func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, updatedMessage:
}
}
-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)
+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)
if case let .chatItemReaction(_, _, reaction) = r { return reaction.chatReaction.chatItem }
throw r.unexpected
}
@@ -512,8 +523,8 @@ func apiGetReactionMembers(groupId: Int64, itemId: Int64, reaction: MsgReaction)
throw r.unexpected
}
-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)
+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)
if case let .chatItemsDeleted(_, items, _) = r { return items }
throw r.unexpected
}
@@ -1237,12 +1248,14 @@ func apiRejectContactRequest(contactReqId: Int64) async throws {
throw r.unexpected
}
-func apiChatRead(type: ChatType, id: Int64) async throws {
- try await sendCommandOkResp(.apiChatRead(type: type, id: id))
+func apiChatRead(type: ChatType, id: Int64, scope: GroupChatScope?) async throws {
+ try await sendCommandOkResp(.apiChatRead(type: type, id: id, scope: scope))
}
-func apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) async throws {
- try await sendCommandOkResp(.apiChatItemsRead(type: type, id: id, itemIds: itemIds))
+func apiChatItemsRead(type: ChatType, id: Int64, scope: GroupChatScope?, itemIds: [Int64]) async throws -> 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 apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
@@ -1545,13 +1558,13 @@ func apiGetNetworkStatuses() throws -> [ConnNetworkStatus] {
throw r.unexpected
}
-func markChatRead(_ chat: Chat) async {
+func markChatRead(_ im: ItemsModel, _ chat: Chat) async {
do {
if chat.chatStats.unreadCount > 0 {
let cInfo = chat.chatInfo
- try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId)
+ try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, scope: cInfo.groupChatScope())
await MainActor.run {
- withAnimation { ChatModel.shared.markAllChatItemsRead(cInfo) }
+ withAnimation { ChatModel.shared.markAllChatItemsRead(im, cInfo) }
}
}
if chat.chatStats.unreadChat {
@@ -1574,11 +1587,12 @@ func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async {
}
}
-func apiMarkChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID], mentionsRead: Int) async {
+func apiMarkChatItemsRead(_ im: ItemsModel, _ cInfo: ChatInfo, _ itemIds: [ChatItem.ID], mentionsRead: Int) async {
do {
- try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, itemIds: itemIds)
- DispatchQueue.main.async {
- ChatModel.shared.markChatItemsRead(cInfo, itemIds, mentionsRead)
+ 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)
}
} catch {
logger.error("apiChatItemsRead error: \(responseError(error))")
@@ -1626,9 +1640,21 @@ func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult {
}
}
-func apiRemoveMembers(_ groupId: Int64, _ memberIds: [Int64], _ withMessages: Bool = false) async throws -> [GroupMember] {
+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 apiDeleteMemberSupportChat(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (GroupInfo, GroupMember) {
+ let r: ChatResponse2 = try await chatSendCmd(.apiDeleteMemberSupportChat(groupId: groupId, groupMemberId: groupMemberId))
+ if case let .memberSupportChatDeleted(_, groupInfo, member) = r { return (groupInfo, member) }
+ throw r.unexpected
+}
+
+func apiRemoveMembers(_ groupId: Int64, _ memberIds: [Int64], _ withMessages: Bool = false) async throws -> (GroupInfo, [GroupMember]) {
let r: ChatResponse2 = try await chatSendCmd(.apiRemoveMembers(groupId: groupId, memberIds: memberIds, withMessages: withMessages), bgTask: false)
- if case let .userDeletedMembers(_, _, members, withMessages) = r { return members }
+ if case let .userDeletedMembers(_, updatedGroupInfo, members, _withMessages) = r { return (updatedGroupInfo, members) }
throw r.unexpected
}
@@ -1669,8 +1695,8 @@ func apiListMembers(_ groupId: Int64) async -> [GroupMember] {
func filterMembersToAdd(_ ms: [GMember]) -> [Contact] {
let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil }
return ChatModel.shared.chats
- .compactMap{ $0.chatInfo.contact }
- .filter{ c in c.sendMsgEnabled && !c.nextSendGrpInv && !memberContactIds.contains(c.apiId) }
+ .compactMap{ c in c.chatInfo.sendMsgEnabled ? c.chatInfo.contact : nil }
+ .filter{ c in !c.nextSendGrpInv && !memberContactIds.contains(c.apiId) }
.sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() }
}
@@ -2132,7 +2158,7 @@ func processReceivedMsg(_ res: ChatEvent) async {
let cInfo = chatItem.chatInfo
let cItem = chatItem.chatItem
if !cItem.isDeletedContent && active(user) {
- await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) }
+ _ = await MainActor.run { m.upsertChatItem(cInfo, cItem) }
}
if let endTask = m.messageDelivery[cItem.id] {
switch cItem.meta.itemStatus {
@@ -2180,6 +2206,9 @@ func processReceivedMsg(_ res: ChatEvent) async {
m.decreaseGroupReportsCounter(item.deletedChatItem.chatInfo.id)
}
}
+ if let updatedChatInfo = items.last?.deletedChatItem.chatInfo {
+ m.updateChatInfo(updatedChatInfo)
+ }
}
case let .groupChatItemsDeleted(user, groupInfo, chatItemIDs, _, member_):
await groupChatItemsDeleted(user, groupInfo, chatItemIDs, member_)
@@ -2228,6 +2257,13 @@ 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 {
@@ -2240,6 +2276,7 @@ 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)
@@ -2249,6 +2286,7 @@ func processReceivedMsg(_ res: ChatEvent) async {
case let .leftMember(user, groupInfo, member):
if active(user) {
await MainActor.run {
+ m.updateGroup(groupInfo)
_ = m.upsertGroupMember(groupInfo, member)
}
}
@@ -2263,6 +2301,12 @@ 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) {
@@ -2549,7 +2593,7 @@ func groupChatItemsDeleted(_ user: UserRef, _ groupInfo: GroupInfo, _ chatItemID
return
}
let im = ItemsModel.shared
- let cInfo = ChatInfo.group(groupInfo: groupInfo)
+ let cInfo = ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil)
await MainActor.run {
m.decreaseGroupReportsCounter(cInfo.id, by: chatItemIDs.count)
}
diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift
index f8d69c5fc8..47c0f61c79 100644
--- a/apps/ios/Shared/SimpleXApp.swift
+++ b/apps/ios/Shared/SimpleXApp.swift
@@ -159,7 +159,7 @@ struct SimpleXApp: App {
if let id = chatModel.chatId,
let chat = chatModel.getChat(id),
!NtfManager.shared.navigatingToChat {
- Task { await loadChat(chat: chat, clearItems: false) }
+ Task { await loadChat(chat: chat, im: ItemsModel.shared, clearItems: false) }
}
if let ncr = chatModel.ntfContactRequest {
await MainActor.run { chatModel.ntfContactRequest = nil }
diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift
index 62a41c504a..b60842a4a0 100644
--- a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift
+++ b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift
@@ -22,11 +22,28 @@ struct ChatInfoToolbar: View {
Image(systemName: "theatermasks").frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.indigo)
Spacer().frame(width: 16)
}
- ChatInfoImage(
- chat: chat,
- size: imageSize,
- color: Color(uiColor: .tertiaryLabel)
- )
+ ZStack(alignment: .bottomTrailing) {
+ ChatInfoImage(
+ chat: chat,
+ size: imageSize,
+ color: Color(uiColor: .tertiaryLabel)
+ )
+ if chat.chatStats.reportsCount > 0 {
+ Image(systemName: "flag.circle.fill")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 14, height: 14)
+ .symbolRenderingMode(.palette)
+ .foregroundStyle(.white, .red)
+ } else if chat.supportUnreadCount > 0 {
+ Image(systemName: "flag.circle.fill")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 14, height: 14)
+ .symbolRenderingMode(.palette)
+ .foregroundStyle(.white, theme.colors.primary)
+ }
+ }
.padding(.trailing, 4)
let t = Text(cInfo.displayName).font(.headline)
(cInfo.contact?.verified == true ? contactVerifiedShield + t : t)
diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift
index 8194c8fe6f..0498dc5d70 100644
--- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift
@@ -687,7 +687,7 @@ struct ChatTTLOption: View {
let m = ChatModel.shared
do {
try await setChatTTL(chatType: chat.chatInfo.chatType, id: chat.chatInfo.apiId, ttl)
- await loadChat(chat: chat, clearItems: true)
+ await loadChat(chat: chat, im: ItemsModel.shared, clearItems: true)
await MainActor.run {
progressIndicator = false
currentChatItemTTL = chatItemTTL
@@ -700,7 +700,7 @@ struct ChatTTLOption: View {
}
catch let error {
logger.error("setChatTTL error \(responseError(error))")
- await loadChat(chat: chat, clearItems: true)
+ await loadChat(chat: chat, im: ItemsModel.shared, clearItems: true)
await MainActor.run {
chatItemTTL = currentChatItemTTL
progressIndicator = false
@@ -938,7 +938,7 @@ struct ChatWallpaperEditorSheet: View {
self.chat = chat
self.themes = if case let ChatInfo.direct(contact) = chat.chatInfo, let uiThemes = contact.uiThemes {
uiThemes
- } else if case let ChatInfo.group(groupInfo) = chat.chatInfo, let uiThemes = groupInfo.uiThemes {
+ } else if case let ChatInfo.group(groupInfo, _) = chat.chatInfo, let uiThemes = groupInfo.uiThemes {
uiThemes
} else {
ThemeModeOverrides()
@@ -974,7 +974,7 @@ struct ChatWallpaperEditorSheet: View {
private func themesFromChat(_ chat: Chat) -> ThemeModeOverrides {
if case let ChatInfo.direct(contact) = chat.chatInfo, let uiThemes = contact.uiThemes {
uiThemes
- } else if case let ChatInfo.group(groupInfo) = chat.chatInfo, let uiThemes = groupInfo.uiThemes {
+ } else if case let ChatInfo.group(groupInfo, _) = chat.chatInfo, let uiThemes = groupInfo.uiThemes {
uiThemes
} else {
ThemeModeOverrides()
@@ -1052,12 +1052,12 @@ struct ChatWallpaperEditorSheet: View {
chat.wrappedValue = Chat.init(chatInfo: ChatInfo.direct(contact: contact))
themes = themesFromChat(chat.wrappedValue)
}
- } else if case var ChatInfo.group(groupInfo) = chat.wrappedValue.chatInfo {
+ } else if case var ChatInfo.group(groupInfo, _) = chat.wrappedValue.chatInfo {
groupInfo.uiThemes = changedThemesConstant
await MainActor.run {
- ChatModel.shared.updateChatInfo(ChatInfo.group(groupInfo: groupInfo))
- chat.wrappedValue = Chat.init(chatInfo: ChatInfo.group(groupInfo: groupInfo))
+ ChatModel.shared.updateChatInfo(ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil))
+ chat.wrappedValue = Chat.init(chatInfo: ChatInfo.group(groupInfo: groupInfo, groupChatScope: nil))
themes = themesFromChat(chat.wrappedValue)
}
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift
index 02be8af73b..b2b4441646 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift
@@ -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 = []
- if var i = m.getChatItemIndex(chatItem) {
+ if var i = m.getChatItemIndex(im, chatItem) {
while i < im.reversedChatItems.count,
let f = featureInfo(im.reversedChatItems[i]) {
if !icons.contains(f.icon) {
@@ -108,6 +108,7 @@ struct CIChatFeatureView_Previews: PreviewProvider {
let enabled = FeatureEnabled(forUser: false, forContact: false)
CIChatFeatureView(
chat: Chat.sampleData,
+ im: ItemsModel.shared,
chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor(.secondary)
).environment(\.revealed, true)
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
index b0b404d8b5..1b9376b5db 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift
@@ -278,6 +278,7 @@ func showFileErrorAlert(_ err: FileError, temporary: Bool = false) {
struct CIFileView_Previews: PreviewProvider {
static var previews: some View {
+ let im = ItemsModel.shared
let sentFile: ChatItem = ChatItem(
chatDir: .directSnd,
meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true),
@@ -293,16 +294,16 @@ struct CIFileView_Previews: PreviewProvider {
file: nil
)
Group {
- ChatItemView(chat: Chat.sampleData, chatItem: sentFile, scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileName: "some_long_file_name_here", fileStatus: .rcvInvitation), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvAccepted), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileStatus: .rcvCancelled), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(fileSize: 1_000_000_000, fileStatus: .rcvInvitation), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Hello there", fileStatus: .rcvInvitation), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getFileMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", fileStatus: .rcvInvitation), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: fileChatItemWtFile, scrollToItemId: { _ in })
+ ChatItemView(chat: Chat.sampleData, im: im, chatItem: sentFile, 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))
}
.environment(\.revealed, false)
.previewLayout(.fixed(width: 360, height: 360))
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift
index d30369339d..d1f49f635a 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift
@@ -12,7 +12,7 @@ import SimpleXChat
struct CIImageView: View {
@EnvironmentObject var m: ChatModel
let chatItem: ChatItem
- var scrollToItemId: ((ChatItem.ID) -> Void)? = nil
+ var scrollToItem: ((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, scrollToItemId: scrollToItemId, image: uiImage, showView: $showFullScreenImage)
+ FullScreenMediaView(chatItem: chatItem, scrollToItem: scrollToItem, image: uiImage, showView: $showFullScreenImage)
}
.if(!smallView) { view in
view.modifier(PrivacyBlur(blurred: $blurred))
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift
index 4e5713c263..3201332c1e 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift
@@ -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 {
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift
index 715e606a74..47aee2a586 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift
@@ -435,6 +435,7 @@ class VoiceItemState {
struct CIVoiceView_Previews: PreviewProvider {
static var previews: some View {
+ let im = ItemsModel.shared
let sentVoiceMessage: ChatItem = ChatItem(
chatDir: .directSnd,
meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true),
@@ -457,10 +458,10 @@ struct CIVoiceView_Previews: PreviewProvider {
duration: 30,
allowMenu: Binding.constant(true)
)
- ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, scrollToItemId: { _ in }, allowMenu: .constant(true))
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), scrollToItemId: { _ in }, allowMenu: .constant(true))
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItemId: { _ in }, allowMenu: .constant(true))
- ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, scrollToItemId: { _ in }, allowMenu: .constant(true))
+ ChatItemView(chat: Chat.sampleData, im: im, chatItem: sentVoiceMessage, 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))
}
.previewLayout(.fixed(width: 360, height: 360))
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift
index f4e2a4135a..0b6f249b9c 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift
@@ -77,6 +77,7 @@ struct FramedCIVoiceView: View {
struct FramedCIVoiceView_Previews: PreviewProvider {
static var previews: some View {
+ let im = ItemsModel.shared
let sentVoiceMessage: ChatItem = ChatItem(
chatDir: .directSnd,
meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true),
@@ -92,11 +93,11 @@ struct FramedCIVoiceView_Previews: PreviewProvider {
file: CIFile.getSample(fileStatus: .sndComplete)
)
Group {
- ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there"), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Hello there", fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWithQuote, scrollToItemId: { _ in })
+ ChatItemView(chat: Chat.sampleData, im: im, chatItem: sentVoiceMessage, 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))
}
.environment(\.revealed, false)
.previewLayout(.fixed(width: 360, height: 360))
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift
index b27d266d8a..c9c9952688 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift
@@ -13,8 +13,10 @@ struct FramedItemView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
+ @ObservedObject var im: ItemsModel
var chatItem: ChatItem
- var scrollToItemId: (ChatItem.ID) -> Void
+ var scrollToItem: (ChatItem.ID) -> Void
+ @Binding var scrollToItemId: ChatItem.ID?
var preview: UIImage?
var maxWidth: CGFloat = .infinity
@State var msgWidth: CGFloat = 0
@@ -56,12 +58,16 @@ struct FramedItemView: View {
if let qi = chatItem.quotedItem {
ciQuoteView(qi)
.simultaneousGesture(TapGesture().onEnded {
- if let ci = ItemsModel.shared.reversedChatItems.first(where: { $0.id == qi.itemId }) {
+ if let ci = im.reversedChatItems.first(where: { $0.id == qi.itemId }) {
withAnimation {
- scrollToItemId(ci.id)
+ scrollToItem(ci.id)
}
} else if let id = qi.itemId {
- scrollToItemId(id)
+ if (chatItem.isReport && im.secondaryIMFilter != nil) {
+ scrollToItemId = id
+ } else {
+ scrollToItem(id)
+ }
} else {
showQuotedItemDoesNotExistAlert()
}
@@ -70,7 +76,7 @@ struct FramedItemView: View {
framedItemHeader(icon: "arrowshape.turn.up.forward", caption: Text(itemForwarded.text(chat.chatInfo.chatType)).italic(), pad: true)
}
- ChatItemContentView(chat: chat, chatItem: chatItem, msgContentView: framedMsgContentView)
+ ChatItemContentView(chat: chat, im: im, chatItem: chatItem, msgContentView: framedMsgContentView)
.padding(chatItem.content.msgContent != nil ? 0 : 4)
.overlay(DetermineWidth())
}
@@ -119,7 +125,7 @@ struct FramedItemView: View {
} else {
switch (chatItem.content.msgContent) {
case let .image(text, _):
- CIImageView(chatItem: chatItem, scrollToItemId: scrollToItemId, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth, showFullScreenImage: $showFullscreenGallery)
+ CIImageView(chatItem: chatItem, scrollToItem: scrollToItem, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth, showFullScreenImage: $showFullscreenGallery)
.overlay(DetermineWidth())
if text == "" && !chatItem.meta.isLive {
Color.clear
@@ -290,7 +296,7 @@ struct FramedItemView: View {
private func membership() -> GroupMember? {
switch chat.chatInfo {
- case let .group(groupInfo: groupInfo): return groupInfo.membership
+ case let .group(groupInfo: groupInfo, _): return groupInfo.membership
default: return nil
}
}
@@ -386,15 +392,16 @@ func chatItemFrameContextColor(_ ci: ChatItem, _ theme: AppTheme) -> Color {
struct FramedItemView_Previews: PreviewProvider {
static var previews: some View {
+ let im = ItemsModel.shared
Group{
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat"), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat"), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
}
.previewLayout(.fixed(width: 360, height: 200))
}
@@ -402,17 +409,18 @@ struct FramedItemView_Previews: PreviewProvider {
struct FramedItemView_Edited_Previews: PreviewProvider {
static var previews: some View {
+ let im = ItemsModel.shared
Group {
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,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"), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,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"), itemEdited: true), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,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"), itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,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"), itemEdited: true), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
}
.environment(\.revealed, false)
.previewLayout(.fixed(width: 360, height: 200))
@@ -421,17 +429,18 @@ struct FramedItemView_Edited_Previews: PreviewProvider {
struct FramedItemView_Deleted_Previews: PreviewProvider {
static var previews: some View {
+ let im = ItemsModel.shared
Group {
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
- FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,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"), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in }, allowMenu: Binding.constant(true))
+ FramedItemView(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), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,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"), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
+ FramedItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,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"), itemDeleted: .deleted(deletedTs: .now)), scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil), allowMenu: Binding.constant(true))
}
.environment(\.revealed, false)
.previewLayout(.fixed(width: 360, height: 200))
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift
index 10e5efa298..f243a83142 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift
@@ -14,7 +14,7 @@ import AVKit
struct FullScreenMediaView: View {
@EnvironmentObject var m: ChatModel
@State var chatItem: ChatItem
- var scrollToItemId: ((ChatItem.ID) -> Void)?
+ var scrollToItem: ((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
- scrollToItemId?(chatItem.id)
+ scrollToItem?(chatItem.id)
} else if w > 60 && w > abs(t.height) * 2 && !scrolling {
let previous = t.width > 0
scrolling = true
diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift
index 87a9b2ce61..c6a5d0353c 100644
--- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift
@@ -14,6 +14,7 @@ struct MarkedDeletedItemView: View {
@EnvironmentObject var theme: AppTheme
@Environment(\.revealed) var revealed: Bool
@ObservedObject var chat: Chat
+ @ObservedObject var im: ItemsModel
var chatItem: ChatItem
var body: some View {
@@ -29,14 +30,14 @@ struct MarkedDeletedItemView: View {
var mergedMarkedDeletedText: LocalizedStringKey {
if !revealed,
let ciCategory = chatItem.mergeCategory,
- var i = m.getChatItemIndex(chatItem) {
+ var i = m.getChatItemIndex(im, chatItem) {
var moderated = 0
var blocked = 0
var blockedByAdmin = 0
var deleted = 0
var moderatedBy: Set = []
- while i < ItemsModel.shared.reversedChatItems.count,
- let ci = .some(ItemsModel.shared.reversedChatItems[i]),
+ while i < im.reversedChatItems.count,
+ let ci = .some(im.reversedChatItems[i]),
ci.mergeCategory == ciCategory,
let itemDeleted = ci.meta.itemDeleted {
switch itemDeleted {
@@ -85,6 +86,7 @@ struct MarkedDeletedItemView_Previews: PreviewProvider {
Group {
MarkedDeletedItemView(
chat: Chat.sampleData,
+ im: ItemsModel.shared,
chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now))
).environment(\.revealed, true)
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift
index f5558bcd93..a412bf4452 100644
--- a/apps/ios/Shared/Views/Chat/ChatItemView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift
@@ -40,25 +40,31 @@ 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 scrollToItemId: (ChatItem.ID) -> Void
+ var scrollToItem: (ChatItem.ID) -> Void
+ @Binding var scrollToItemId: ChatItem.ID?
var maxWidth: CGFloat = .infinity
@Binding var allowMenu: Bool
init(
chat: Chat,
+ im: ItemsModel,
chatItem: ChatItem,
- scrollToItemId: @escaping (ChatItem.ID) -> Void,
+ scrollToItem: @escaping (ChatItem.ID) -> Void,
+ scrollToItemId: Binding = .constant(nil),
showMember: Bool = false,
maxWidth: CGFloat = .infinity,
allowMenu: Binding = .constant(false)
) {
self.chat = chat
+ self.im = im
self.chatItem = chatItem
- self.scrollToItemId = scrollToItemId
+ self.scrollToItem = scrollToItem
+ _scrollToItemId = scrollToItemId
self.maxWidth = maxWidth
_allowMenu = allowMenu
}
@@ -66,14 +72,14 @@ struct ChatItemView: View {
var body: some View {
let ci = chatItem
if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) {
- MarkedDeletedItemView(chat: chat, chatItem: chatItem)
+ MarkedDeletedItemView(chat: chat, im: im, chatItem: chatItem)
} else if ci.quotedItem == nil && ci.meta.itemForwarded == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive {
if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) {
EmojiItemView(chat: chat, chatItem: ci)
} else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent {
CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: $allowMenu)
} else if ci.content.msgContent == nil {
- ChatItemContentView(chat: chat, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
+ ChatItemContentView(chat: chat, im: im, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case
} else {
framedItemView()
}
@@ -101,8 +107,10 @@ struct ChatItemView: View {
}()
return FramedItemView(
chat: chat,
+ im: im,
chatItem: chatItem,
- scrollToItemId: scrollToItemId,
+ scrollToItem: scrollToItem,
+ scrollToItemId: $scrollToItemId,
preview: preview,
maxWidth: maxWidth,
imgWidth: adjustedMaxWidth,
@@ -117,6 +125,7 @@ struct ChatItemContentView: 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
@@ -140,7 +149,9 @@ struct ChatItemContentView: 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()
@@ -149,7 +160,7 @@ struct ChatItemContentView: View {
case let .rcvChatPreference(feature, allowed, param):
CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param)
case let .sndChatPreference(feature, _, _):
- CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary)
+ CIChatFeatureView(chat: chat, im: im, chatItem: chatItem, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary)
case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary))
case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary))
case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red)
@@ -181,6 +192,13 @@ struct ChatItemContentView: 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)
@@ -196,7 +214,7 @@ struct ChatItemContentView: View {
}
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
- CIChatFeatureView(chat: chat, chatItem: chatItem, feature: feature, iconColor: iconColor)
+ CIChatFeatureView(chat: chat, im: im, chatItem: chatItem, feature: feature, iconColor: iconColor)
}
private var mergedGroupEventText: Text? {
@@ -256,16 +274,17 @@ func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text {
struct ChatItemView_Previews: PreviewProvider {
static var previews: some View {
+ let im = ItemsModel.shared
Group{
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too"), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), scrollToItemId: { _ in })
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), scrollToItemId: { _ in }).environment(\.revealed, true)
- ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), scrollToItemId: { _ in }).environment(\.revealed, true)
+ ChatItemView(chat: Chat.sampleData, im: im, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), 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)
}
.environment(\.revealed, false)
.previewLayout(.fixed(width: 360, height: 70))
@@ -275,10 +294,12 @@ struct ChatItemView_Previews: PreviewProvider {
struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
static var previews: some View {
+ let im = ItemsModel.shared
let ciFeatureContent = CIContent.rcvChatFeature(feature: .fullDelete, enabled: FeatureEnabled(forUser: false, forContact: false), param: nil)
Group{
ChatItemView(
chat: Chat.sampleData,
+ im: im,
chatItem: ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
@@ -286,10 +307,12 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
quotedItem: nil,
file: nil
),
- scrollToItemId: { _ in }
+ scrollToItem: { _ in },
+ scrollToItemId: Binding.constant(nil)
)
ChatItemView(
chat: Chat.sampleData,
+ im: im,
chatItem: ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, "1 skipped message", .rcvRead),
@@ -297,10 +320,11 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
quotedItem: nil,
file: nil
),
- scrollToItemId: { _ in }
+ scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil)
)
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)),
@@ -308,10 +332,12 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
quotedItem: nil,
file: nil
),
- scrollToItemId: { _ in }
+ scrollToItem: { _ in },
+ scrollToItemId: Binding.constant(nil)
)
ChatItemView(
chat: Chat.sampleData,
+ im: im,
chatItem: ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, "group event text", .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
@@ -319,10 +345,12 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
quotedItem: nil,
file: nil
),
- scrollToItemId: { _ in }
+ scrollToItem: { _ in },
+ scrollToItemId: Binding.constant(nil)
)
ChatItemView(
chat: Chat.sampleData,
+ im: im,
chatItem: ChatItem(
chatDir: .directRcv,
meta: CIMeta.getSample(1, .now, ciFeatureContent.text, .rcvRead, itemDeleted: .deleted(deletedTs: .now)),
@@ -330,7 +358,8 @@ struct ChatItemView_NonMsgContentDeleted_Previews: PreviewProvider {
quotedItem: nil,
file: nil
),
- scrollToItemId: { _ in }
+ scrollToItem: { _ in },
+ scrollToItemId: Binding.constant(nil)
)
}
.environment(\.revealed, true)
diff --git a/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift b/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift
index 07034cf8ec..93ecf870eb 100644
--- a/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItemsLoader.swift
@@ -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 = { 0 ... 0 }
@@ -22,7 +22,7 @@ func apiLoadMessages(
let chat: Chat
let navInfo: NavigationInfo
do {
- (chat, navInfo) = try await apiGetChat(chatId: chatId, pagination: pagination, search: search)
+ (chat, navInfo) = try await apiGetChat(chatId: chatId, scope: im.groupScopeInfo?.toChatScope(), contentTag: im.contentTag, pagination: pagination, search: search)
} catch let error {
logger.error("apiLoadMessages error: \(responseError(error))")
return
@@ -38,30 +38,31 @@ func apiLoadMessages(
return
}
- let unreadAfterItemId = chatState.unreadAfterItemId
+ let unreadAfterItemId = im.chatState.unreadAfterItemId
- let oldItems = Array(ItemsModel.shared.reversedChatItems.reversed())
+ let oldItems = Array(im.reversedChatItems.reversed())
var newItems: [ChatItem] = []
switch pagination {
case .initial:
let newSplits: [Int64] = if !chat.chatItems.isEmpty && navInfo.afterTotal > 0 { [chat.chatItems.last!.id] } else { [] }
- if chatModel.getChat(chat.id) == nil {
+ if im.secondaryIMFilter == nil && chatModel.getChat(chat.id) == nil {
chatModel.addChat(chat)
}
await MainActor.run {
- chatModel.chatItemStatuses.removeAll()
- ItemsModel.shared.reversedChatItems = chat.chatItems.reversed()
- chatModel.updateChatInfo(chat.chatInfo)
- chatState.splits = newSplits
- if !chat.chatItems.isEmpty {
- chatState.unreadAfterItemId = chat.chatItems.last!.id
+ im.reversedChatItems = chat.chatItems.reversed()
+ if im.secondaryIMFilter == nil {
+ chatModel.updateChatInfo(chat.chatInfo)
}
- chatState.totalAfter = navInfo.afterTotal
- chatState.unreadTotal = chat.chatStats.unreadCount
- chatState.unreadAfter = navInfo.afterUnread
- chatState.unreadAfterNewestLoaded = navInfo.afterUnread
+ im.chatState.splits = newSplits
+ if !chat.chatItems.isEmpty {
+ im.chatState.unreadAfterItemId = chat.chatItems.last!.id
+ }
+ im.chatState.totalAfter = navInfo.afterTotal
+ im.chatState.unreadTotal = chat.chatStats.unreadCount
+ im.chatState.unreadAfter = navInfo.afterUnread
+ im.chatState.unreadAfterNewestLoaded = navInfo.afterUnread
- PreloadState.shared.clear()
+ im.preloadState.clear()
}
case let .before(paginationChatItemId, _):
newItems.append(contentsOf: oldItems)
@@ -71,15 +72,15 @@ func apiLoadMessages(
let wasSize = newItems.count
let visibleItemIndexes = await MainActor.run { visibleItemIndexesNonReversed() }
let modifiedSplits = removeDuplicatesAndModifySplitsOnBeforePagination(
- unreadAfterItemId, &newItems, newIds, chatState.splits, visibleItemIndexes
+ unreadAfterItemId, &newItems, newIds, im.chatState.splits, visibleItemIndexes
)
let insertAt = max((indexInCurrentItems - (wasSize - newItems.count) + modifiedSplits.trimmedIds.count), 0)
newItems.insert(contentsOf: chat.chatItems, at: insertAt)
let newReversed: [ChatItem] = newItems.reversed()
await MainActor.run {
- ItemsModel.shared.reversedChatItems = newReversed
- chatState.splits = modifiedSplits.newSplits
- chatState.moveUnreadAfterItem(modifiedSplits.oldUnreadSplitIndex, modifiedSplits.newUnreadSplitIndex, oldItems)
+ im.reversedChatItems = newReversed
+ im.chatState.splits = modifiedSplits.newSplits
+ im.chatState.moveUnreadAfterItem(modifiedSplits.oldUnreadSplitIndex, modifiedSplits.newUnreadSplitIndex, oldItems)
}
case let .after(paginationChatItemId, _):
newItems.append(contentsOf: oldItems)
@@ -89,7 +90,7 @@ func apiLoadMessages(
let mappedItems = mapItemsToIds(chat.chatItems)
let newIds = mappedItems.0
let (newSplits, unreadInLoaded) = removeDuplicatesAndModifySplitsOnAfterPagination(
- mappedItems.1, paginationChatItemId, &newItems, newIds, chat, chatState.splits
+ mappedItems.1, paginationChatItemId, &newItems, newIds, chat, im.chatState.splits
)
let indexToAdd = min(indexInCurrentItems + 1, newItems.count)
let indexToAddIsLast = indexToAdd == newItems.count
@@ -97,19 +98,19 @@ func apiLoadMessages(
let new: [ChatItem] = newItems
let newReversed: [ChatItem] = newItems.reversed()
await MainActor.run {
- ItemsModel.shared.reversedChatItems = newReversed
- chatState.splits = newSplits
- chatState.moveUnreadAfterItem(chatState.splits.first ?? new.last!.id, new)
+ im.reversedChatItems = newReversed
+ im.chatState.splits = newSplits
+ im.chatState.moveUnreadAfterItem(im.chatState.splits.first ?? new.last!.id, new)
// loading clear bottom area, updating number of unread items after the newest loaded item
if indexToAddIsLast {
- chatState.unreadAfterNewestLoaded -= unreadInLoaded
+ im.chatState.unreadAfterNewestLoaded -= unreadInLoaded
}
}
case .around:
var newSplits: [Int64]
if openAroundItemId == nil {
newItems.append(contentsOf: oldItems)
- newSplits = await removeDuplicatesAndUpperSplits(&newItems, chat, chatState.splits, visibleItemIndexesNonReversed)
+ newSplits = await removeDuplicatesAndUpperSplits(&newItems, chat, im.chatState.splits, visibleItemIndexesNonReversed)
} else {
newSplits = []
}
@@ -120,33 +121,37 @@ func apiLoadMessages(
let newReversed: [ChatItem] = newItems.reversed()
let orderedSplits = newSplits
await MainActor.run {
- ItemsModel.shared.reversedChatItems = newReversed
- chatState.splits = orderedSplits
- chatState.unreadAfterItemId = chat.chatItems.last!.id
- chatState.totalAfter = navInfo.afterTotal
- chatState.unreadTotal = chat.chatStats.unreadCount
- chatState.unreadAfter = navInfo.afterUnread
+ im.reversedChatItems = newReversed
+ im.chatState.splits = orderedSplits
+ im.chatState.unreadAfterItemId = chat.chatItems.last!.id
+ im.chatState.totalAfter = navInfo.afterTotal
+ im.chatState.unreadTotal = chat.chatStats.unreadCount
+ im.chatState.unreadAfter = navInfo.afterUnread
if let openAroundItemId {
- chatState.unreadAfterNewestLoaded = navInfo.afterUnread
- ChatModel.shared.openAroundItemId = openAroundItemId
- ChatModel.shared.chatId = chatId
+ im.chatState.unreadAfterNewestLoaded = navInfo.afterUnread
+ if im.secondaryIMFilter == nil {
+ ChatModel.shared.openAroundItemId = openAroundItemId // TODO [knocking] move openAroundItemId from ChatModel to ItemsModel?
+ ChatModel.shared.chatId = chat.id
+ }
} else {
// no need to set it, count will be wrong
// chatState.unreadAfterNewestLoaded = navInfo.afterUnread
}
- PreloadState.shared.clear()
+ im.preloadState.clear()
}
case .last:
newItems.append(contentsOf: oldItems)
- let newSplits = await removeDuplicatesAndUnusedSplits(&newItems, chat, chatState.splits)
+ let newSplits = await removeDuplicatesAndUnusedSplits(&newItems, chat, im.chatState.splits)
newItems.append(contentsOf: chat.chatItems)
let items = newItems
await MainActor.run {
- ItemsModel.shared.reversedChatItems = items.reversed()
- chatState.splits = newSplits
- chatModel.updateChatInfo(chat.chatInfo)
- chatState.unreadAfterNewestLoaded = 0
+ im.reversedChatItems = items.reversed()
+ im.chatState.splits = newSplits
+ if im.secondaryIMFilter == nil {
+ chatModel.updateChatInfo(chat.chatInfo)
+ }
+ im.chatState.unreadAfterNewestLoaded = 0
}
}
}
diff --git a/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift b/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift
index 0a55ed48cc..5f2102b8bc 100644
--- a/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift
+++ b/apps/ios/Shared/Views/Chat/ChatItemsMerger.swift
@@ -10,6 +10,7 @@ import SwiftUI
import SimpleXChat
struct MergedItems: Hashable, Equatable {
+ let im: ItemsModel
let items: [MergedItem]
let splits: [SplitRange]
// chat item id, index in list
@@ -23,15 +24,15 @@ struct MergedItems: Hashable, Equatable {
hasher.combine("\(items.hashValue)")
}
- static func create(_ items: [ChatItem], _ revealedItems: Set, _ chatState: ActiveChatState) -> MergedItems {
- if items.isEmpty {
- return MergedItems(items: [], splits: [], indexInParentItems: [:])
+ static func create(_ im: ItemsModel, _ revealedItems: Set) -> MergedItems {
+ if im.reversedChatItems.isEmpty {
+ return MergedItems(im: im, items: [], splits: [], indexInParentItems: [:])
}
- let unreadCount = chatState.unreadTotal
+ let unreadCount = im.chatState.unreadTotal
- let unreadAfterItemId = chatState.unreadAfterItemId
- let itemSplits = chatState.splits
+ let unreadAfterItemId = im.chatState.unreadAfterItemId
+ let itemSplits = im.chatState.splits
var mergedItems: [MergedItem] = []
// Indexes of splits here will be related to reversedChatItems, not chatModel.chatItems
var splitRanges: [SplitRange] = []
@@ -40,19 +41,19 @@ struct MergedItems: Hashable, Equatable {
var unclosedSplitIndex: Int? = nil
var unclosedSplitIndexInParent: Int? = nil
var visibleItemIndexInParent = -1
- var unreadBefore = unreadCount - chatState.unreadAfterNewestLoaded
+ var unreadBefore = unreadCount - im.chatState.unreadAfterNewestLoaded
var lastRevealedIdsInMergedItems: BoxedValue<[Int64]>? = nil
var lastRangeInReversedForMergedItems: BoxedValue>? = nil
var recent: MergedItem? = nil
- while index < items.count {
- let item = items[index]
- let prev = index >= 1 ? items[index - 1] : nil
- let next = index + 1 < items.count ? items[index + 1] : nil
+ while index < im.reversedChatItems.count {
+ let item = im.reversedChatItems[index]
+ let prev = index >= 1 ? im.reversedChatItems[index - 1] : nil
+ let next = index + 1 < im.reversedChatItems.count ? im.reversedChatItems[index + 1] : nil
let category = item.mergeCategory
let itemIsSplit = itemSplits.contains(item.id)
if item.id == unreadAfterItemId {
- unreadBefore = unreadCount - chatState.unreadAfter
+ unreadBefore = unreadCount - im.chatState.unreadAfter
}
if item.isRcvNew {
unreadBefore -= 1
@@ -106,18 +107,19 @@ struct MergedItems: Hashable, Equatable {
// found item that is considered as a split
if let unclosedSplitIndex, let unclosedSplitIndexInParent {
// it was at least second split in the list
- splitRanges.append(SplitRange(itemId: items[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index - 1, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent - 1))
+ splitRanges.append(SplitRange(itemId: im.reversedChatItems[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index - 1, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent - 1))
}
unclosedSplitIndex = index
unclosedSplitIndexInParent = visibleItemIndexInParent
- } else if index + 1 == items.count, let unclosedSplitIndex, let unclosedSplitIndexInParent {
+ } else if index + 1 == im.reversedChatItems.count, let unclosedSplitIndex, let unclosedSplitIndexInParent {
// just one split for the whole list, there will be no more, it's the end
- splitRanges.append(SplitRange(itemId: items[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent))
+ splitRanges.append(SplitRange(itemId: im.reversedChatItems[unclosedSplitIndex].id, indexRangeInReversed: unclosedSplitIndex ... index, indexRangeInParentItems: unclosedSplitIndexInParent ... visibleItemIndexInParent))
}
indexInParentItems[item.id] = visibleItemIndexInParent
index += 1
}
return MergedItems(
+ im: im,
items: mergedItems,
splits: splitRanges,
indexInParentItems: indexInParentItems
@@ -127,7 +129,6 @@ struct MergedItems: Hashable, Equatable {
// Use this check to ensure that mergedItems state based on currently actual state of global
// splits and reversedChatItems
func isActualState() -> Bool {
- let im = ItemsModel.shared
// do not load anything if global splits state is different than in merged items because it
// will produce undefined results in terms of loading and placement of items.
// Same applies to reversedChatItems
@@ -434,7 +435,7 @@ class BoxedValue: Equatable, Hashable {
}
@MainActor
-func visibleItemIndexesNonReversed(_ listState: EndlessScrollView.ListState, _ mergedItems: MergedItems) -> ClosedRange {
+func visibleItemIndexesNonReversed(_ im: ItemsModel, _ listState: EndlessScrollView.ListState, _ mergedItems: MergedItems) -> ClosedRange {
let zero = 0 ... 0
let items = mergedItems.items
if items.isEmpty {
@@ -445,12 +446,12 @@ func visibleItemIndexesNonReversed(_ listState: EndlessScrollView.Li
guard let newest, let oldest else {
return zero
}
- let size = ItemsModel.shared.reversedChatItems.count
+ let size = im.reversedChatItems.count
let range = size - oldest ... size - newest
if range.lowerBound < 0 || range.upperBound < 0 {
return zero
}
- // visible items mapped to their underlying data structure which is ItemsModel.shared.reversedChatItems.reversed()
+ // visible items mapped to their underlying data structure which is im.reversedChatItems.reversed()
return range
}
diff --git a/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift
index c1a1eec7d2..2fb1c3fb35 100644
--- a/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift
+++ b/apps/ios/Shared/Views/Chat/ChatScrollHelpers.swift
@@ -9,7 +9,7 @@
import SwiftUI
import SimpleXChat
-func loadLastItems(_ loadingMoreItems: Binding, loadingBottomItems: Binding, _ chat: Chat) async {
+func loadLastItems(_ loadingMoreItems: Binding, loadingBottomItems: Binding, _ chat: Chat, _ im: ItemsModel) async {
await MainActor.run {
loadingMoreItems.wrappedValue = true
loadingBottomItems.wrappedValue = true
@@ -22,27 +22,15 @@ func loadLastItems(_ loadingMoreItems: Binding, loadingBottomItems: Bindin
}
return
}
- await apiLoadMessages(chat.chatInfo.id, ChatPagination.last(count: 50), ItemsModel.shared.chatState)
+ await apiLoadMessages(chat.chatInfo.id, im, ChatPagination.last(count: 50))
await MainActor.run {
loadingMoreItems.wrappedValue = false
loadingBottomItems.wrappedValue = false
}
}
-class PreloadState {
- static let shared = PreloadState()
- var prevFirstVisible: Int64 = Int64.min
- var prevItemsCount: Int = 0
- var preloading: Bool = false
-
- func clear() {
- prevFirstVisible = Int64.min
- prevItemsCount = 0
- preloading = false
- }
-}
-
func preloadIfNeeded(
+ _ im: ItemsModel,
_ allowLoadMoreItems: Binding,
_ ignoreLoadingRequests: Binding,
_ listState: EndlessScrollView.ListState,
@@ -50,7 +38,7 @@ func preloadIfNeeded(
loadItems: @escaping (Bool, ChatPagination) async -> Bool,
loadLastItems: @escaping () async -> Void
) {
- let state = PreloadState.shared
+ let state = im.preloadState
guard !listState.isScrolling && !listState.isAnimatedScrolling,
!state.preloading,
listState.totalItemsCount > 0
@@ -63,7 +51,7 @@ func preloadIfNeeded(
Task {
defer { state.preloading = false }
var triedToLoad = true
- await preloadItems(mergedItems.boxedValue, allowLoadMore, listState, ignoreLoadingRequests) { pagination in
+ await preloadItems(im, mergedItems.boxedValue, allowLoadMore, listState, ignoreLoadingRequests) { pagination in
triedToLoad = await loadItems(false, pagination)
return triedToLoad
}
@@ -73,11 +61,11 @@ func preloadIfNeeded(
}
// it's important to ask last items when the view is fully covered with items. Otherwise, visible items from one
// split will be merged with last items and position of scroll will change unexpectedly.
- if listState.itemsCanCoverScreen && !ItemsModel.shared.lastItemsLoaded {
+ if listState.itemsCanCoverScreen && !im.lastItemsLoaded {
await loadLastItems()
}
}
- } else if listState.itemsCanCoverScreen && !ItemsModel.shared.lastItemsLoaded {
+ } else if listState.itemsCanCoverScreen && !im.lastItemsLoaded {
state.preloading = true
Task {
defer { state.preloading = false }
@@ -87,6 +75,7 @@ func preloadIfNeeded(
}
func preloadItems(
+ _ im: ItemsModel,
_ mergedItems: MergedItems,
_ allowLoadMoreItems: Bool,
_ listState: EndlessScrollView.ListState,
@@ -105,7 +94,7 @@ async {
let splits = mergedItems.splits
let lastVisibleIndex = listState.lastVisibleItemIndex
var lastIndexToLoadFrom: Int? = findLastIndexToLoadFromInSplits(firstVisibleIndex, lastVisibleIndex, remaining, splits)
- let items: [ChatItem] = ItemsModel.shared.reversedChatItems.reversed()
+ let items: [ChatItem] = im.reversedChatItems.reversed()
if splits.isEmpty && !items.isEmpty && lastVisibleIndex > mergedItems.items.count - remaining {
lastIndexToLoadFrom = items.count - 1
}
@@ -122,7 +111,7 @@ async {
let sizeWas = items.count
let firstItemIdWas = items.first?.id
let triedToLoad = await loadItems(ChatPagination.before(chatItemId: loadFromItemId, count: ChatPagination.PRELOAD_COUNT))
- if triedToLoad && sizeWas == ItemsModel.shared.reversedChatItems.count && firstItemIdWas == ItemsModel.shared.reversedChatItems.last?.id {
+ if triedToLoad && sizeWas == im.reversedChatItems.count && firstItemIdWas == im.reversedChatItems.last?.id {
ignoreLoadingRequests.wrappedValue = loadFromItemId
return false
}
@@ -133,7 +122,7 @@ async {
let splits = mergedItems.splits
let split = splits.last(where: { $0.indexRangeInParentItems.contains(firstVisibleIndex) })
// we're inside a splitRange (top --- [end of the splitRange --- we're here --- start of the splitRange] --- bottom)
- let reversedItems: [ChatItem] = ItemsModel.shared.reversedChatItems
+ let reversedItems: [ChatItem] = im.reversedChatItems
if let split, split.indexRangeInParentItems.lowerBound + remaining > firstVisibleIndex {
let index = split.indexRangeInReversed.lowerBound
if index >= 0 {
diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift
index c136ebc01b..8ce0c50849 100644
--- a/apps/ios/Shared/Views/Chat/ChatView.swift
+++ b/apps/ios/Shared/Views/Chat/ChatView.swift
@@ -15,8 +15,6 @@ private let memberImageSize: CGFloat = 34
struct ChatView: View {
@EnvironmentObject var chatModel: ChatModel
- @ObservedObject var im = ItemsModel.shared
- @State var mergedItems: BoxedValue = BoxedValue(MergedItems.create(ItemsModel.shared.reversedChatItems, [], ItemsModel.shared.chatState))
@State var revealedItems: Set = Set()
@State var theme: AppTheme = buildTheme()
@Environment(\.dismiss) var dismiss
@@ -24,6 +22,10 @@ struct ChatView: View {
@Environment(\.presentationMode) var presentationMode
@Environment(\.scenePhase) var scenePhase
@State @ObservedObject var chat: Chat
+ @ObservedObject var im: ItemsModel
+ @State var mergedItems: BoxedValue
+ @State var floatingButtonModel: FloatingButtonModel
+ @Binding var scrollToItemId: ChatItem.ID?
@State private var showChatInfoSheet: Bool = false
@State private var showAddMembersSheet: Bool = false
@State private var composeState = ComposeState()
@@ -55,12 +57,14 @@ struct ChatView: View {
@State private var allowLoadMoreItems: Bool = false
@State private var ignoreLoadingRequests: Int64? = nil
@State private var animatedScrollingInProgress: Bool = false
- @State private var floatingButtonModel: FloatingButtonModel = FloatingButtonModel()
+ @State private var showUserSupportChatSheet = false
@State private var scrollView: EndlessScrollView = EndlessScrollView(frame: .zero)
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
+ let userSupportScopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: nil)
+
var body: some View {
if #available(iOS 16.0, *) {
viewBody
@@ -73,25 +77,39 @@ struct ChatView: View {
private var viewBody: some View {
let cInfo = chat.chatInfo
+ let memberSupportChat: (groupInfo: GroupInfo, member: GroupMember?)? =
+ if case let .group(groupInfo, .memberSupport(member)) = cInfo {
+ (groupInfo, member)
+ } else {
+ nil
+ }
+ let userMemberKnockingChat = memberSupportChat?.groupInfo.membership.memberPending == true
return ZStack {
let wallpaperImage = theme.wallpaper.type.image
let wallpaperType = theme.wallpaper.type
let backgroundColor = theme.wallpaper.background ?? wallpaperType.defaultBackgroundColor(theme.base, theme.colors.background)
let tintColor = theme.wallpaper.tint ?? wallpaperType.defaultTintColor(theme.base)
Color.clear.ignoresSafeArea(.all)
- .if(wallpaperImage != nil) { view in
+ .if(wallpaperImage != nil && im.secondaryIMFilter == nil) { view in
view.modifier(
ChatViewBackground(image: wallpaperImage!, imageType: wallpaperType, background: backgroundColor, tint: tintColor)
)
}
VStack(spacing: 0) {
ZStack(alignment: .bottomTrailing) {
- chatItemsList()
- if let groupInfo = chat.chatInfo.groupInfo, !composeState.message.isEmpty {
- GroupMentionsView(groupInfo: groupInfo, composeState: $composeState, selectedRange: $selectedRange, keyboardVisible: $keyboardVisible)
+ if userMemberKnockingChat {
+ ZStack(alignment: .top) {
+ chatItemsList()
+ userMemberKnockingTitleBar()
+ }
+ } else {
+ chatItemsList()
}
- FloatingButtons(theme: theme, scrollView: scrollView, chat: chat, loadingMoreItems: $loadingMoreItems, loadingTopItems: $loadingTopItems, requestedTopScroll: $requestedTopScroll, loadingBottomItems: $loadingBottomItems, requestedBottomScroll: $requestedBottomScroll, animatedScrollingInProgress: $animatedScrollingInProgress, listState: scrollView.listState, model: floatingButtonModel, reloadItems: {
- mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState)
+ if let groupInfo = chat.chatInfo.groupInfo, !composeState.message.isEmpty {
+ GroupMentionsView(im: im, groupInfo: groupInfo, composeState: $composeState, selectedRange: $selectedRange, keyboardVisible: $keyboardVisible)
+ }
+ FloatingButtons(im: im, theme: theme, scrollView: scrollView, chat: chat, loadingMoreItems: $loadingMoreItems, loadingTopItems: $loadingTopItems, requestedTopScroll: $requestedTopScroll, loadingBottomItems: $loadingBottomItems, requestedBottomScroll: $requestedBottomScroll, animatedScrollingInProgress: $animatedScrollingInProgress, listState: scrollView.listState, model: floatingButtonModel, reloadItems: {
+ mergedItems.boxedValue = MergedItems.create(im, revealedItems)
scrollView.updateItems(mergedItems.boxedValue.items)
}
)
@@ -101,6 +119,7 @@ struct ChatView: View {
let reason = chat.chatInfo.userCantSendReason
ComposeView(
chat: chat,
+ im: im,
composeState: $composeState,
keyboardVisible: $keyboardVisible,
keyboardHiddenDate: $keyboardHiddenDate,
@@ -118,7 +137,7 @@ struct ChatView: View {
}
} else {
SelectedItemsBottomToolbar(
- chatItems: ItemsModel.shared.reversedChatItems,
+ im: im,
selectedChatItems: $selectedChatItems,
chatInfo: chat.chatInfo,
deleteItems: { forAll in
@@ -129,7 +148,7 @@ struct ChatView: View {
showArchiveSelectedReports = true
},
moderateItems: {
- if case let .group(groupInfo) = chat.chatInfo {
+ if case let .group(groupInfo, _) = chat.chatInfo {
showModerateSelectedMessagesAlert(groupInfo)
}
},
@@ -148,7 +167,11 @@ struct ChatView: View {
}
.background(ToolbarMaterial.material(toolbarMaterial))
}
- .navigationTitle(cInfo.chatViewName)
+ .navigationTitle(
+ memberSupportChat == nil
+ ? cInfo.chatViewName
+ : memberSupportChat?.member?.chatViewName ?? NSLocalizedString("Chat with admins", comment: "chat toolbar")
+ )
.background(theme.colors.background)
.navigationBarTitleDisplayMode(.inline)
.environmentObject(theme)
@@ -172,7 +195,7 @@ struct ChatView: View {
archiveReports(chat.chatInfo, selected.sorted(), false, deletedSelectedMessages)
}
}
- if case let ChatInfo.group(groupInfo) = chat.chatInfo, groupInfo.membership.memberActive {
+ if case let ChatInfo.group(groupInfo, _) = chat.chatInfo, groupInfo.membership.memberActive {
Button("For all moderators", role: .destructive) {
if let selected = selectedChatItems {
archiveReports(chat.chatInfo, selected.sorted(), true, deletedSelectedMessages)
@@ -181,23 +204,20 @@ struct ChatView: View {
}
}
.appSheet(item: $selectedMember) { member in
- Group {
- if case let .group(groupInfo) = chat.chatInfo {
- GroupMemberInfoView(
- groupInfo: groupInfo,
- chat: chat,
- groupMember: member,
- navigation: true
- )
- }
+ if case let .group(groupInfo, _) = chat.chatInfo {
+ GroupMemberInfoView(
+ groupInfo: groupInfo,
+ chat: chat,
+ groupMember: member,
+ scrollToItemId: $scrollToItemId,
+ navigation: true
+ )
}
}
// it should be presented on top level in order to prevent a bug in SwiftUI on iOS 16 related to .focused() modifier in AddGroupMembersView's search field
.appSheet(isPresented: $showAddMembersSheet) {
- Group {
- if case let .group(groupInfo) = cInfo {
- AddGroupMembersView(chat: chat, groupInfo: groupInfo)
- }
+ if case let .group(groupInfo, _) = cInfo {
+ AddGroupMembersView(chat: chat, groupInfo: groupInfo)
}
}
.sheet(isPresented: Binding(
@@ -216,6 +236,21 @@ struct ChatView: View {
ChatItemForwardingView(chatItems: forwardedChatItems, fromChatInfo: chat.chatInfo, composeState: $composeState)
}
}
+ .appSheet(
+ isPresented: $showUserSupportChatSheet,
+ onDismiss: {
+ if chat.chatInfo.groupInfo?.membership.memberPending ?? false {
+ chatModel.chatId = nil
+ }
+ }
+ ) {
+ if let groupInfo = cInfo.groupInfo {
+ SecondaryChatView(
+ chat: Chat(chatInfo: .group(groupInfo: groupInfo, groupChatScope: userSupportScopeInfo), chatItems: [], chatStats: ChatStats()),
+ scrollToItemId: $scrollToItemId
+ )
+ }
+ }
.onAppear {
scrollView.listState.onUpdateListener = onChatItemsUpdated
selectedChatItems = nil
@@ -231,6 +266,14 @@ struct ChatView: View {
}
}
}
+ // if this is the main chat of the group with the pending member (knocking)
+ if case let .group(groupInfo, nil) = chat.chatInfo,
+ groupInfo.membership.memberPending {
+ ItemsModel.loadSecondaryChat(chat.id, chatFilter: .groupChatScopeContext(groupScopeInfo: userSupportScopeInfo)) {
+ showUserSupportChatSheet = true
+ chatModel.secondaryPendingInviteeChatOpened = true
+ }
+ }
}
.onChange(of: chatModel.chatId) { cId in
showChatInfoSheet = false
@@ -245,7 +288,7 @@ struct ChatView: View {
initChatView()
theme = buildTheme()
closeSearch()
- mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState)
+ mergedItems.boxedValue = MergedItems.create(im, revealedItems)
scrollView.updateItems(mergedItems.boxedValue.items)
if let openAround = chatModel.openAroundItemId, let index = mergedItems.boxedValue.indexInParentItems[openAround] {
@@ -262,10 +305,15 @@ struct ChatView: View {
dismiss()
}
}
+ .onChange(of: chatModel.secondaryPendingInviteeChatOpened) { opened in
+ if im.secondaryIMFilter != nil && !opened {
+ dismiss()
+ }
+ }
.onChange(of: chatModel.openAroundItemId) { openAround in
if let openAround {
closeSearch()
- mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState)
+ mergedItems.boxedValue = MergedItems.create(im, revealedItems)
scrollView.updateItems(mergedItems.boxedValue.items)
chatModel.openAroundItemId = nil
@@ -288,9 +336,8 @@ struct ChatView: View {
if chatModel.chatId == cInfo.id && !presentationMode.wrappedValue.isPresented {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
if chatModel.chatId == nil {
- chatModel.chatItemStatuses = [:]
- ItemsModel.shared.reversedChatItems = []
- ItemsModel.shared.chatState.clear()
+ im.reversedChatItems = []
+ im.chatState.clear()
chatModel.groupMembers = []
chatModel.groupMembersIndexes.removeAll()
chatModel.membersLoaded = false
@@ -303,124 +350,220 @@ struct ChatView: View {
}
.toolbar {
ToolbarItem(placement: .principal) {
- if selectedChatItems != nil {
- SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems)
- } else if case let .direct(contact) = cInfo {
- Button {
- Task {
- showChatInfoSheet = true
- }
- } label: {
- ChatInfoToolbar(chat: chat)
- }
- .appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) {
- ChatInfoView(
- chat: chat,
- contact: contact,
- localAlias: chat.chatInfo.localAlias,
- featuresAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences),
- currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences),
- onSearch: { focusSearch() }
- )
- }
- } else if case let .group(groupInfo) = cInfo {
- Button {
- Task { await chatModel.loadGroupMembers(groupInfo) { showChatInfoSheet = true } }
- } label: {
- ChatInfoToolbar(chat: chat)
- .tint(theme.colors.primary)
- }
- .appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) {
- GroupChatInfoView(
- chat: chat,
- groupInfo: Binding(
- get: { groupInfo },
- set: { gInfo in
- chat.chatInfo = .group(groupInfo: gInfo)
- chat.created = Date.now
- }
- ),
- onSearch: { focusSearch() },
- localAlias: groupInfo.localAlias
- )
- }
- } else if case .local = cInfo {
- ChatInfoToolbar(chat: chat)
+ if im.secondaryIMFilter == nil {
+ primaryPrincipalToolbarContent()
+ } else if !userMemberKnockingChat { // no toolbar while knocking chat, it's unstable on sheet
+ secondaryPrincipalToolbarContent()
}
}
ToolbarItem(placement: .navigationBarTrailing) {
- if selectedChatItems != nil {
- Button {
- withAnimation {
- selectedChatItems = nil
- }
- } label: {
- Text("Cancel")
- }
- } else {
- switch cInfo {
- case let .direct(contact):
- HStack {
- let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
- if callsPrefEnabled {
- if chatModel.activeCall == nil {
- callButton(contact, .audio, imageName: "phone")
- .disabled(!contact.ready || !contact.active)
- } else if let call = chatModel.activeCall, call.contact.id == cInfo.id {
- endCallButton(call)
- }
- }
- Menu {
- if callsPrefEnabled && chatModel.activeCall == nil {
- Button {
- CallController.shared.startCall(contact, .video)
- } label: {
- Label("Video call", systemImage: "video")
- }
- .disabled(!contact.ready || !contact.active)
- }
- searchButton()
- ToggleNtfsButton(chat: chat)
- .disabled(!contact.ready || !contact.active)
- } label: {
- Image(systemName: "ellipsis")
- }
- }
- case let .group(groupInfo):
- HStack {
- if groupInfo.canAddMembers {
- if (chat.chatInfo.incognito) {
- groupLinkButton()
- .appSheet(isPresented: $showGroupLinkSheet) {
- GroupLinkView(
- groupId: groupInfo.groupId,
- groupLink: $groupLink,
- groupLinkMemberRole: $groupLinkMemberRole,
- showTitle: true,
- creatingGroup: false
- )
- }
- } else {
- addMembersButton()
- }
- }
- Menu {
- searchButton()
- ToggleNtfsButton(chat: chat)
- } label: {
- Image(systemName: "ellipsis")
- }
- }
- case .local:
- searchButton()
- default:
- EmptyView()
+ if im.secondaryIMFilter == nil {
+ primaryTrailingToolbarContent()
+ } else if !userMemberKnockingChat {
+ secondaryTrailingToolbarContent()
+ }
+ }
+ }
+ .if(im.secondaryIMFilter == nil) { v in
+ v.onChange(of: scrollToItemId) { itemId in
+ if let itemId = itemId {
+ dismissAllSheets(animated: false) {
+ scrollToItem(itemId)
+ scrollToItemId = nil
}
}
}
}
}
+ @inline(__always)
+ @ViewBuilder private func primaryPrincipalToolbarContent() -> some View {
+ let cInfo = chat.chatInfo
+ if selectedChatItems != nil {
+ SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems)
+ } else if case let .direct(contact) = cInfo {
+ Button {
+ Task {
+ showChatInfoSheet = true
+ }
+ } label: {
+ ChatInfoToolbar(chat: chat)
+ }
+ .appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) {
+ ChatInfoView(
+ chat: chat,
+ contact: contact,
+ localAlias: chat.chatInfo.localAlias,
+ featuresAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences),
+ currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences),
+ onSearch: { focusSearch() }
+ )
+ }
+ } else if case let .group(groupInfo, _) = cInfo {
+ Button {
+ Task { await chatModel.loadGroupMembers(groupInfo) { showChatInfoSheet = true } }
+ } label: {
+ ChatInfoToolbar(chat: chat)
+ .tint(theme.colors.primary)
+ }
+ .appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) {
+ GroupChatInfoView(
+ chat: chat,
+ groupInfo: Binding(
+ get: { groupInfo },
+ set: { gInfo in
+ chat.chatInfo = .group(groupInfo: gInfo, groupChatScope: nil)
+ chat.created = Date.now
+ }
+ ),
+ scrollToItemId: $scrollToItemId,
+ onSearch: { focusSearch() },
+ localAlias: groupInfo.localAlias
+ )
+ }
+ } else if case .local = cInfo {
+ ChatInfoToolbar(chat: chat)
+ }
+ }
+
+ @inline(__always)
+ @ViewBuilder private func primaryTrailingToolbarContent() -> some View {
+ let cInfo = chat.chatInfo
+ if selectedChatItems != nil {
+ Button {
+ withAnimation {
+ selectedChatItems = nil
+ }
+ } label: {
+ Text("Cancel")
+ }
+ } else {
+ switch cInfo {
+ case let .direct(contact):
+ HStack {
+ let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
+ if callsPrefEnabled {
+ if chatModel.activeCall == nil {
+ callButton(contact, .audio, imageName: "phone")
+ .disabled(!contact.ready || !contact.active)
+ } else if let call = chatModel.activeCall, call.contact.id == cInfo.id {
+ endCallButton(call)
+ }
+ }
+ Menu {
+ if callsPrefEnabled && chatModel.activeCall == nil {
+ Button {
+ CallController.shared.startCall(contact, .video)
+ } label: {
+ Label("Video call", systemImage: "video")
+ }
+ .disabled(!contact.ready || !contact.active)
+ }
+ searchButton()
+ ToggleNtfsButton(chat: chat)
+ .disabled(!contact.ready || !contact.active)
+ } label: {
+ Image(systemName: "ellipsis")
+ }
+ }
+ case let .group(groupInfo, _):
+ HStack {
+ if groupInfo.canAddMembers {
+ if (chat.chatInfo.incognito) {
+ groupLinkButton()
+ .appSheet(isPresented: $showGroupLinkSheet) {
+ GroupLinkView(
+ groupId: groupInfo.groupId,
+ groupLink: $groupLink,
+ groupLinkMemberRole: $groupLinkMemberRole,
+ showTitle: true,
+ creatingGroup: false
+ )
+ }
+ } else {
+ addMembersButton()
+ }
+ }
+ Menu {
+ searchButton()
+ ToggleNtfsButton(chat: chat)
+ } label: {
+ Image(systemName: "ellipsis")
+ }
+ }
+ case .local:
+ searchButton()
+ default:
+ EmptyView()
+ }
+ }
+ }
+
+ @inline(__always)
+ @ViewBuilder private func secondaryPrincipalToolbarContent() -> some View {
+ if selectedChatItems != nil {
+ SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems)
+ } else {
+ switch im.secondaryIMFilter {
+ case let .groupChatScopeContext(groupScopeInfo):
+ switch groupScopeInfo {
+ case let .memberSupport(groupMember_):
+ if let groupMember = groupMember_ {
+ MemberSupportChatToolbar(groupMember: groupMember)
+ } else {
+ textChatToolbar("Chat with admins")
+ }
+ }
+ case let .msgContentTagContext(contentTag):
+ switch contentTag {
+ case .report:
+ textChatToolbar("Member reports")
+ default:
+ EmptyView()
+ }
+ case .none:
+ EmptyView()
+ }
+ }
+ }
+
+ @inline(__always)
+ @ViewBuilder private func secondaryTrailingToolbarContent() -> some View {
+ if selectedChatItems != nil {
+ Button {
+ withAnimation {
+ selectedChatItems = nil
+ }
+ } label: {
+ Text("Cancel")
+ }
+ } else {
+ searchButton()
+ }
+ }
+
+ @inline(__always)
+ private func userMemberKnockingTitleBar() -> some View {
+ VStack(spacing: 0) {
+ Text("Chat with admins")
+ .font(.headline)
+ .foregroundColor(theme.colors.onBackground)
+ .padding(.top, 8)
+ .padding(.bottom, 14)
+ .frame(maxWidth: .infinity)
+ .background(ToolbarMaterial.material(toolbarMaterial))
+ Divider()
+ }
+ }
+
+ func textChatToolbar(_ text: LocalizedStringKey) -> some View {
+ Text(text)
+ .font(.headline)
+ .lineLimit(1)
+ .foregroundColor(theme.colors.onBackground)
+ .frame(width: 220)
+ }
+
private func initChatView() {
let cInfo = chat.chatInfo
// This check prevents the call to apiContactInfo after the app is suspended, and the database is closed.
@@ -451,19 +594,19 @@ struct ChatView: View {
floatingButtonModel.updateOnListChange(scrollView.listState)
}
- private func scrollToItemId(_ itemId: ChatItem.ID) {
+ private func scrollToItem(_ itemId: ChatItem.ID) {
Task {
do {
var index = mergedItems.boxedValue.indexInParentItems[itemId]
if index == nil {
let pagination = ChatPagination.around(chatItemId: itemId, count: ChatPagination.PRELOAD_COUNT * 2)
- let oldSize = ItemsModel.shared.reversedChatItems.count
+ let oldSize = im.reversedChatItems.count
let triedToLoad = await loadChatItems(chat, pagination)
if !triedToLoad {
return
}
var repeatsLeft = 50
- while oldSize == ItemsModel.shared.reversedChatItems.count && repeatsLeft > 0 {
+ while oldSize == im.reversedChatItems.count && repeatsLeft > 0 {
try await Task.sleep(nanoseconds: 20_000000)
repeatsLeft -= 1
}
@@ -473,7 +616,7 @@ struct ChatView: View {
closeKeyboardAndRun {
Task {
await MainActor.run { animatedScrollingInProgress = true }
- await scrollView.scrollToItemAnimated(min(ItemsModel.shared.reversedChatItems.count - 1, index))
+ await scrollView.scrollToItemAnimated(min(im.reversedChatItems.count - 1, index))
await MainActor.run { animatedScrollingInProgress = false }
}
}
@@ -548,11 +691,13 @@ struct ChatView: View {
? (g.size.width - 32)
: (g.size.width - 32) * 0.84
return ChatItemWithMenu(
+ im: im,
chat: $chat,
index: index,
isLastItem: index == mergedItems.boxedValue.items.count - 1,
chatItem: ci,
- scrollToItemId: scrollToItemId,
+ scrollToItem: scrollToItem,
+ scrollToItemId: $scrollToItemId,
merged: mergedItem,
maxWidth: maxWidth,
composeState: $composeState,
@@ -580,7 +725,7 @@ struct ChatView: View {
}
}
.onChange(of: im.reversedChatItems) { items in
- mergedItems.boxedValue = MergedItems.create(items, revealedItems, im.chatState)
+ mergedItems.boxedValue = MergedItems.create(im, revealedItems)
scrollView.updateItems(mergedItems.boxedValue.items)
if im.itemAdded {
im.itemAdded = false
@@ -592,7 +737,7 @@ struct ChatView: View {
}
}
.onChange(of: revealedItems) { revealed in
- mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealed, im.chatState)
+ mergedItems.boxedValue = MergedItems.create(im, revealed)
scrollView.updateItems(mergedItems.boxedValue.items)
}
.onChange(of: chat.id) { _ in
@@ -627,7 +772,7 @@ struct ChatView: View {
private func updateWithInitiallyLoadedItems() {
if mergedItems.boxedValue.items.isEmpty {
- mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, ItemsModel.shared.chatState)
+ mergedItems.boxedValue = MergedItems.create(im, revealedItems)
}
let unreadIndex = mergedItems.boxedValue.items.lastIndex(where: { $0.hasUnread() })
let unreadItemId: Int64? = if let unreadIndex { mergedItems.boxedValue.items[unreadIndex].newest().item.id } else { nil }
@@ -647,8 +792,8 @@ struct ChatView: View {
private func searchTextChanged(_ s: String) {
Task {
- await loadChat(chat: chat, search: s)
- mergedItems.boxedValue = MergedItems.create(im.reversedChatItems, revealedItems, im.chatState)
+ await loadChat(chat: chat, im: im, search: s)
+ mergedItems.boxedValue = MergedItems.create(im, revealedItems)
await MainActor.run {
scrollView.updateItems(mergedItems.boxedValue.items)
}
@@ -663,79 +808,8 @@ struct ChatView: View {
}
}
- class FloatingButtonModel: ObservableObject {
- @Published var unreadAbove: Int = 0
- @Published var unreadBelow: Int = 0
- @Published var isNearBottom: Bool = true
- @Published var date: Date? = nil
- @Published var isDateVisible: Bool = false
- var hideDateWorkItem: DispatchWorkItem? = nil
-
- func updateOnListChange(_ listState: EndlessScrollView.ListState) {
- let lastVisibleItem = oldestPartiallyVisibleListItemInListStateOrNull(listState)
- let unreadBelow = if let lastVisibleItem {
- max(0, ItemsModel.shared.chatState.unreadTotal - lastVisibleItem.unreadBefore)
- } else {
- 0
- }
- let unreadAbove = ItemsModel.shared.chatState.unreadTotal - unreadBelow
- let date: Date? =
- if let lastVisible = listState.visibleItems.last {
- Calendar.current.startOfDay(for: lastVisible.item.oldest().item.meta.itemTs)
- } else {
- nil
- }
-
- // set the counters and date indicator
- DispatchQueue.main.async { [weak self] in
- guard let it = self else { return }
- it.setDate(visibility: true)
- it.unreadAbove = unreadAbove
- it.unreadBelow = unreadBelow
- it.date = date
- }
-
- // set floating button indication mode
- let nearBottom = listState.firstVisibleItemIndex < 1
- if nearBottom != self.isNearBottom {
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in
- self?.isNearBottom = nearBottom
- }
- }
-
- // hide Date indicator after 1 second of no scrolling
- hideDateWorkItem?.cancel()
- let workItem = DispatchWorkItem { [weak self] in
- guard let it = self else { return }
- it.setDate(visibility: false)
- it.hideDateWorkItem = nil
- }
- DispatchQueue.main.async { [weak self] in
- self?.hideDateWorkItem = workItem
- DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: workItem)
- }
- }
-
- func resetDate() {
- date = nil
- isDateVisible = false
- }
-
- private func setDate(visibility isVisible: Bool) {
- if isVisible {
- if !isNearBottom,
- !isDateVisible,
- let date, !Calendar.current.isDateInToday(date) {
- withAnimation { self.isDateVisible = true }
- }
- } else if isDateVisible {
- withAnimation { self.isDateVisible = false }
- }
- }
-
- }
-
private struct FloatingButtons: View {
+ @ObservedObject var im: ItemsModel
let theme: AppTheme
let scrollView: EndlessScrollView
let chat: Chat
@@ -780,7 +854,7 @@ struct ChatView: View {
.contextMenu {
Button {
Task {
- await markChatRead(chat)
+ await markChatRead(im, chat)
}
} label: {
Label("Mark read", systemImage: "checkmark")
@@ -805,7 +879,7 @@ struct ChatView: View {
}
}
.onTapGesture {
- if loadingBottomItems || !ItemsModel.shared.lastItemsLoaded {
+ if loadingBottomItems || !im.lastItemsLoaded {
requestedTopScroll = false
requestedBottomScroll = true
} else {
@@ -825,7 +899,7 @@ struct ChatView: View {
}
}
.onChange(of: loadingBottomItems) { loading in
- if !loading && requestedBottomScroll && ItemsModel.shared.lastItemsLoaded {
+ if !loading && requestedBottomScroll && im.lastItemsLoaded {
requestedBottomScroll = false
scrollToBottom()
}
@@ -835,9 +909,9 @@ struct ChatView: View {
private func scrollToTopUnread() {
Task {
- if !ItemsModel.shared.chatState.splits.isEmpty {
+ if !im.chatState.splits.isEmpty {
await MainActor.run { loadingMoreItems = true }
- await loadChat(chatId: chat.id, openAroundItemId: nil, clearItems: false)
+ await loadChat(chatId: chat.id, im: im, openAroundItemId: nil, clearItems: false)
await MainActor.run { reloadItems() }
if let index = listState.items.lastIndex(where: { $0.hasUnread() }) {
await MainActor.run { animatedScrollingInProgress = true }
@@ -947,7 +1021,7 @@ struct ChatView: View {
private func addMembersButton() -> some View {
Button {
- if case let .group(gInfo) = chat.chatInfo {
+ if case let .group(gInfo, _) = chat.chatInfo {
Task { await chatModel.loadGroupMembers(gInfo) { showAddMembersSheet = true } }
}
} label: {
@@ -957,7 +1031,7 @@ struct ChatView: View {
private func groupLinkButton() -> some View {
Button {
- if case let .group(gInfo) = chat.chatInfo {
+ if case let .group(gInfo, _) = chat.chatInfo {
Task {
do {
if let link = try apiGetGroupLink(gInfo.groupId) {
@@ -1008,6 +1082,7 @@ struct ChatView: View {
let (validItems, confirmation) = try await apiPlanForwardChatItems(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
+ scope: chat.chatInfo.groupChatScope(),
itemIds: Array(selectedChatItems)
)
if let confirmation {
@@ -1097,7 +1172,6 @@ struct ChatView: View {
}
func openForwardingSheet(_ items: [Int64]) async {
- let im = ItemsModel.shared
var items = Set(items)
var fci = [ChatItem]()
for reversedChatItem in im.reversedChatItems {
@@ -1136,11 +1210,11 @@ struct ChatView: View {
private func loadChatItemsUnchecked(_ chat: Chat, _ pagination: ChatPagination) async -> Bool {
await apiLoadMessages(
chat.chatInfo.id,
+ im,
pagination,
- im.chatState,
searchText,
nil,
- { visibleItemIndexesNonReversed(scrollView.listState, mergedItems.boxedValue) }
+ { visibleItemIndexesNonReversed(im, scrollView.listState, mergedItems.boxedValue) }
)
return true
}
@@ -1152,11 +1226,12 @@ struct ChatView: View {
func onChatItemsUpdated() {
if !mergedItems.boxedValue.isActualState() {
- //logger.debug("Items are not actual, waiting for the next update: \(String(describing: mergedItems.boxedValue.splits)) \(ItemsModel.shared.chatState.splits), \(mergedItems.boxedValue.indexInParentItems.count) vs \(ItemsModel.shared.reversedChatItems.count)")
+ //logger.debug("Items are not actual, waiting for the next update: \(String(describing: mergedItems.boxedValue.splits)) \(im.chatState.splits), \(mergedItems.boxedValue.indexInParentItems.count) vs \(im.reversedChatItems.count)")
return
}
floatingButtonModel.updateOnListChange(scrollView.listState)
preloadIfNeeded(
+ im,
$allowLoadMoreItems,
$ignoreLoadingRequests,
scrollView.listState,
@@ -1170,13 +1245,14 @@ struct ChatView: View {
},
loadLastItems: {
if !loadingMoreItems {
- await loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat)
+ await loadLastItems($loadingMoreItems, loadingBottomItems: $loadingBottomItems, chat, im)
}
}
)
}
private struct ChatItemWithMenu: View {
+ @ObservedObject var im: ItemsModel
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileRadius = defaultProfileImageCorner
@@ -1185,7 +1261,8 @@ struct ChatView: View {
let index: Int
let isLastItem: Bool
let chatItem: ChatItem
- let scrollToItemId: (ChatItem.ID) -> Void
+ let scrollToItem: (ChatItem.ID) -> Void
+ @Binding var scrollToItemId: ChatItem.ID?
let merged: MergedItem
let maxWidth: CGFloat
@Binding var composeState: ComposeState
@@ -1261,8 +1338,6 @@ struct ChatView: View {
}
var body: some View {
- let im = ItemsModel.shared
-
let last = isLastItem ? im.reversedChatItems.last : nil
let listItem = merged.newest()
let item = listItem.item
@@ -1306,12 +1381,12 @@ struct ChatView: View {
let (itemIds, unreadMentions) = unreadItemIds(range)
if !itemIds.isEmpty {
waitToMarkRead {
- await apiMarkChatItemsRead(chat.chatInfo, itemIds, mentionsRead: unreadMentions)
+ await apiMarkChatItemsRead(im, chat.chatInfo, itemIds, mentionsRead: unreadMentions)
}
}
} else if chatItem.isRcvNew {
waitToMarkRead {
- await apiMarkChatItemsRead(chat.chatInfo, [chatItem.id], mentionsRead: chatItem.meta.userMention ? 1 : 0)
+ await apiMarkChatItemsRead(im, chat.chatInfo, [chatItem.id], mentionsRead: chatItem.meta.userMention ? 1 : 0)
}
}
}
@@ -1333,7 +1408,6 @@ struct ChatView: View {
}
private func unreadItemIds(_ range: ClosedRange) -> ([ChatItem.ID], Int) {
- let im = ItemsModel.shared
var unreadItems: [ChatItem.ID] = []
var unreadMentions: Int = 0
@@ -1546,8 +1620,10 @@ struct ChatView: View {
}
ChatItemView(
chat: chat,
+ im: im,
chatItem: ci,
- scrollToItemId: scrollToItemId,
+ scrollToItem: scrollToItem,
+ scrollToItemId: $scrollToItemId,
maxWidth: maxWidth,
allowMenu: $allowMenu
)
@@ -1587,7 +1663,7 @@ struct ChatView: View {
self.archivingReports = []
}
}
- if case let ChatInfo.group(groupInfo) = chat.chatInfo, groupInfo.membership.memberActive {
+ if case let ChatInfo.group(groupInfo, _) = chat.chatInfo, groupInfo.membership.memberActive {
Button("For all moderators", role: .destructive) {
if let reports = self.archivingReports {
archiveReports(chat.chatInfo, reports.sorted(), true)
@@ -1636,7 +1712,7 @@ struct ChatView: View {
})
}
switch chat.chatInfo {
- case let .group(groupInfo):
+ case let .group(groupInfo, _):
v.contextMenu {
ReactionContextMenu(
groupInfo: groupInfo,
@@ -1659,7 +1735,7 @@ struct ChatView: View {
@ViewBuilder
private func menu(_ ci: ChatItem, _ range: ClosedRange?, live: Bool) -> some View {
- if case let .group(gInfo) = chat.chatInfo, ci.isReport, ci.meta.itemDeleted == nil {
+ if case let .group(gInfo, _) = chat.chatInfo, ci.isReport, ci.meta.itemDeleted == nil {
if ci.chatDir != .groupSnd, gInfo.membership.memberRole >= .moderator {
archiveReportButton(ci)
}
@@ -1718,7 +1794,7 @@ struct ChatView: View {
if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) {
moderateButton(ci, groupInfo)
} else if ci.meta.itemDeleted == nil && chat.groupFeatureEnabled(.reports),
- case let .group(gInfo) = chat.chatInfo,
+ case let .group(gInfo, _) = chat.chatInfo,
gInfo.membership.memberRole == .member
&& !live
&& composeState.voiceMessageRecordingState == .noRecording {
@@ -1829,6 +1905,7 @@ struct ChatView: View {
let chatItem = try await apiChatItemReaction(
type: cInfo.chatType,
id: cInfo.apiId,
+ scope: cInfo.groupChatScope(),
itemId: ci.id,
add: add,
reaction: reaction
@@ -1942,11 +2019,11 @@ struct ChatView: View {
Task {
do {
let cInfo = chat.chatInfo
- let ciInfo = try await apiGetChatItemInfo(type: cInfo.chatType, id: cInfo.apiId, itemId: ci.id)
+ let ciInfo = try await apiGetChatItemInfo(type: cInfo.chatType, id: cInfo.apiId, scope: cInfo.groupChatScope(), itemId: ci.id)
await MainActor.run {
chatItemInfo = ciInfo
}
- if case let .group(gInfo) = chat.chatInfo {
+ if case let .group(gInfo, _) = chat.chatInfo {
await m.loadGroupMembers(gInfo)
}
} catch let error {
@@ -2000,13 +2077,13 @@ struct ChatView: View {
private func deleteButton(_ ci: ChatItem, label: LocalizedStringKey = "Delete") -> Button {
Button(role: .destructive) {
if !revealed,
- let currIndex = m.getChatItemIndex(ci),
+ let currIndex = m.getChatItemIndex(im, ci),
let ciCategory = ci.mergeCategory {
let (prevHidden, _) = m.getPrevShownChatItem(currIndex, ciCategory)
if let range = itemsRange(currIndex, prevHidden) {
var itemIds: [Int64] = []
for i in range {
- itemIds.append(ItemsModel.shared.reversedChatItems[i].id)
+ itemIds.append(im.reversedChatItems[i].id)
}
showDeleteMessages = true
deletingItems = itemIds
@@ -2144,12 +2221,12 @@ struct ChatView: View {
selectedChatItems = selectedChatItems ?? []
var itemIds: [Int64] = []
if !revealed,
- let currIndex = m.getChatItemIndex(ci),
+ let currIndex = m.getChatItemIndex(im, ci),
let ciCategory = ci.mergeCategory {
let (prevHidden, _) = m.getPrevShownChatItem(currIndex, ciCategory)
if let range = itemsRange(currIndex, prevHidden) {
for i in range {
- itemIds.append(ItemsModel.shared.reversedChatItems[i].id)
+ itemIds.append(im.reversedChatItems[i].id)
}
} else {
itemIds.append(ci.id)
@@ -2183,6 +2260,7 @@ struct ChatView: View {
try await apiDeleteChatItems(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
+ scope: chat.chatInfo.groupChatScope(),
itemIds: [di.id],
mode: mode
)
@@ -2199,6 +2277,7 @@ struct ChatView: View {
if deletedItem.isActiveReport {
m.decreaseGroupReportsCounter(chat.chatInfo.id)
}
+ m.updateChatInfo(itemDeletion.deletedChatItem.chatInfo)
}
}
}
@@ -2237,14 +2316,14 @@ struct ChatView: View {
if searchIsNotBlank {
goToItemInnerButton(alignStart, "magnifyingglass", touchInProgress: touchInProgress) {
closeKeyboardAndRun {
- ItemsModel.shared.loadOpenChatNoWait(chat.id, chatItem.id)
+ im.loadOpenChatNoWait(chat.id, chatItem.id)
}
}
} else if let chatTypeApiIdMsgId {
goToItemInnerButton(alignStart, "arrow.right", touchInProgress: touchInProgress) {
closeKeyboardAndRun {
let (chatType, apiId, msgId) = chatTypeApiIdMsgId
- ItemsModel.shared.loadOpenChatNoWait("\(chatType.rawValue)\(apiId)", msgId)
+ im.loadOpenChatNoWait("\(chatType.rawValue)\(apiId)", msgId)
}
}
}
@@ -2271,6 +2350,84 @@ struct ChatView: View {
}
}
+class FloatingButtonModel: ObservableObject {
+ @ObservedObject var im: ItemsModel
+
+ public init(im: ItemsModel) {
+ self.im = im
+ }
+
+ @Published var unreadAbove: Int = 0
+ @Published var unreadBelow: Int = 0
+ @Published var isNearBottom: Bool = true
+ @Published var date: Date? = nil
+ @Published var isDateVisible: Bool = false
+ var hideDateWorkItem: DispatchWorkItem? = nil
+
+ func updateOnListChange(_ listState: EndlessScrollView.ListState) {
+ let lastVisibleItem = oldestPartiallyVisibleListItemInListStateOrNull(listState)
+ let unreadBelow = if let lastVisibleItem {
+ max(0, im.chatState.unreadTotal - lastVisibleItem.unreadBefore)
+ } else {
+ 0
+ }
+ let unreadAbove = im.chatState.unreadTotal - unreadBelow
+ let date: Date? =
+ if let lastVisible = listState.visibleItems.last {
+ Calendar.current.startOfDay(for: lastVisible.item.oldest().item.meta.itemTs)
+ } else {
+ nil
+ }
+
+ // set the counters and date indicator
+ DispatchQueue.main.async { [weak self] in
+ guard let it = self else { return }
+ it.setDate(visibility: true)
+ it.unreadAbove = unreadAbove
+ it.unreadBelow = unreadBelow
+ it.date = date
+ }
+
+ // set floating button indication mode
+ let nearBottom = listState.firstVisibleItemIndex < 1
+ if nearBottom != self.isNearBottom {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { [weak self] in
+ self?.isNearBottom = nearBottom
+ }
+ }
+
+ // hide Date indicator after 1 second of no scrolling
+ hideDateWorkItem?.cancel()
+ let workItem = DispatchWorkItem { [weak self] in
+ guard let it = self else { return }
+ it.setDate(visibility: false)
+ it.hideDateWorkItem = nil
+ }
+ DispatchQueue.main.async { [weak self] in
+ self?.hideDateWorkItem = workItem
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: workItem)
+ }
+ }
+
+ func resetDate() {
+ date = nil
+ isDateVisible = false
+ }
+
+ private func setDate(visibility isVisible: Bool) {
+ if isVisible {
+ if !isNearBottom,
+ !isDateVisible,
+ let date, !Calendar.current.isDateInToday(date) {
+ withAnimation { self.isDateVisible = true }
+ }
+ } else if isDateVisible {
+ withAnimation { self.isDateVisible = false }
+ }
+ }
+
+}
+
private func broadcastDeleteButtonText(_ chat: Chat) -> LocalizedStringKey {
chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone"
}
@@ -2292,6 +2449,7 @@ private func deleteMessages(_ chat: Chat, _ deletingItems: [Int64], _ mode: CIDe
try await apiDeleteChatItems(
type: chatInfo.chatType,
id: chatInfo.apiId,
+ scope: chatInfo.groupChatScope(),
itemIds: itemIds,
mode: mode
)
@@ -2309,6 +2467,9 @@ private func deleteMessages(_ chat: Chat, _ deletingItems: [Int64], _ mode: CIDe
ChatModel.shared.decreaseGroupReportsCounter(chat.chatInfo.id)
}
}
+ if let updatedChatInfo = deletedItems.last?.deletedChatItem.chatInfo {
+ ChatModel.shared.updateChatInfo(updatedChatInfo)
+ }
}
await onSuccess()
} catch {
@@ -2340,6 +2501,9 @@ func archiveReports(_ chatInfo: ChatInfo, _ itemIds: [Int64], _ forAll: Bool, _
ChatModel.shared.decreaseGroupReportsCounter(chatInfo.id)
}
}
+ if let updatedChatInfo = deleted.last?.deletedChatItem.chatInfo {
+ ChatModel.shared.updateChatInfo(updatedChatInfo)
+ }
}
await onSuccess()
} catch {
@@ -2353,7 +2517,7 @@ private func buildTheme() -> AppTheme {
if let cId = ChatModel.shared.chatId, let chat = ChatModel.shared.getChat(cId) {
let perChatTheme = if case let .direct(contact) = chat.chatInfo {
contact.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight)
- } else if case let .group(groupInfo) = chat.chatInfo {
+ } else if case let .group(groupInfo, _) = chat.chatInfo {
groupInfo.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight)
} else {
nil as ThemeModeOverride?
@@ -2506,7 +2670,7 @@ func updateChatSettings(_ chat: Chat, chatSettings: ChatSettings) {
case var .direct(contact):
contact.chatSettings = chatSettings
ChatModel.shared.updateContact(contact)
- case var .group(groupInfo):
+ case var .group(groupInfo, _):
groupInfo.chatSettings = chatSettings
ChatModel.shared.updateGroup(groupInfo)
default: ()
@@ -2523,7 +2687,8 @@ struct ChatView_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
chatModel.chatId = "@1"
- ItemsModel.shared.reversedChatItems = [
+ let im = ItemsModel.shared
+ im.reversedChatItems = [
ChatItem.getSample(1, .directSnd, .now, "hello"),
ChatItem.getSample(2, .directRcv, .now, "hi"),
ChatItem.getSample(3, .directRcv, .now, "hi there"),
@@ -2535,7 +2700,13 @@ struct ChatView_Previews: PreviewProvider {
ChatItem.getSample(9, .directSnd, .now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
]
@State var showChatInfo = false
- return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []))
- .environmentObject(chatModel)
+ return ChatView(
+ chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []),
+ im: im,
+ mergedItems: BoxedValue(MergedItems.create(im, [])),
+ floatingButtonModel: FloatingButtonModel(im: im),
+ scrollToItemId: Binding.constant(nil)
+ )
+ .environmentObject(chatModel)
}
}
diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
index 8993de886f..68a2f6d7b1 100644
--- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
+++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift
@@ -323,6 +323,7 @@ struct ComposeView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
+ @ObservedObject var im: ItemsModel
@Binding var composeState: ComposeState
@Binding var keyboardVisible: Bool
@Binding var keyboardHiddenDate: Date
@@ -356,6 +357,20 @@ struct ComposeView: View {
var body: some View {
VStack(spacing: 0) {
Divider()
+ if let groupInfo = chat.chatInfo.groupInfo,
+ case let .groupChatScopeContext(groupScopeInfo) = im.secondaryIMFilter,
+ case let .memberSupport(member) = groupScopeInfo,
+ let member = member,
+ member.memberPending,
+ composeState.contextItem == .noContextItem,
+ composeState.noPreview {
+ ContextPendingMemberActionsView(
+ groupInfo: groupInfo,
+ member: member
+ )
+ Divider()
+ }
+
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
ContextInvitingContactMemberView()
Divider()
@@ -366,8 +381,8 @@ struct ComposeView: View {
Divider()
}
// preference checks should match checks in forwarding list
- let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
- let fileProhibited = composeState.attachmentPreview && !chat.groupFeatureEnabled(.files)
+ let simplexLinkProhibited = im.secondaryIMFilter == nil && hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
+ let fileProhibited = im.secondaryIMFilter == nil && composeState.attachmentPreview && !chat.groupFeatureEnabled(.files)
let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice)
if simplexLinkProhibited {
msgNotAllowedView("SimpleX links not allowed", icon: "link")
@@ -397,7 +412,8 @@ struct ComposeView: View {
.padding(.bottom, 16)
.padding(.leading, 12)
.tint(theme.colors.primary)
- if case let .group(g) = chat.chatInfo,
+ if im.secondaryIMFilter == nil,
+ case let .group(g, _) = chat.chatInfo,
!g.fullGroupPreferences.files.on(for: g.membership) {
b.disabled(true).onTapGesture {
AlertManager.shared.showAlertMsg(
@@ -438,8 +454,8 @@ struct ComposeView: View {
keyboardVisible: $keyboardVisible,
keyboardHiddenDate: $keyboardHiddenDate,
sendButtonColor: chat.chatInfo.incognito
- ? .indigo.opacity(colorScheme == .dark ? 1 : 0.7)
- : theme.colors.primary
+ ? .indigo.opacity(colorScheme == .dark ? 1 : 0.7)
+ : theme.colors.primary
)
.padding(.trailing, 12)
.disabled(!chat.chatInfo.sendMsgEnabled)
@@ -939,6 +955,7 @@ struct ComposeView: View {
let chatItem = try await apiUpdateChatItem(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
+ scope: chat.chatInfo.groupChatScope(),
itemId: ei.id,
updatedMessage: UpdatedMessage(msgContent: mc, mentions: composeState.memberMentions),
live: live
@@ -1001,9 +1018,9 @@ struct ComposeView: View {
reportReason: reportReason,
reportText: msgText
) {
- await MainActor.run {
- for chatItem in chatItems {
- chatModel.addChatItem(chat.chatInfo, chatItem)
+ if showReportsInSupportChatAlertDefault.get() {
+ await MainActor.run {
+ showReportsInSupportChatAlert()
}
}
return chatItems.first
@@ -1011,7 +1028,27 @@ 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)],
@@ -1026,6 +1063,7 @@ struct ComposeView: View {
: await apiSendMessages(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
+ scope: chat.chatInfo.groupChatScope(),
live: live,
ttl: ttl,
composedMessages: msgs
@@ -1050,8 +1088,10 @@ struct ComposeView: View {
if let chatItems = await apiForwardChatItems(
toChatType: chat.chatInfo.chatType,
toChatId: chat.chatInfo.apiId,
+ toScope: chat.chatInfo.groupChatScope(),
fromChatType: fromChatInfo.chatType,
fromChatId: fromChatInfo.apiId,
+ fromScope: fromChatInfo.groupChatScope(),
itemIds: forwardedItems.map { $0.id },
ttl: ttl
) {
@@ -1272,12 +1312,14 @@ struct ComposeView: View {
struct ComposeView_Previews: PreviewProvider {
static var previews: some View {
let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
+ let im = ItemsModel.shared
@State var composeState = ComposeState(message: "hello")
@State var selectedRange = NSRange()
return Group {
ComposeView(
chat: chat,
+ im: im,
composeState: $composeState,
keyboardVisible: Binding.constant(true),
keyboardHiddenDate: Binding.constant(Date.now),
@@ -1286,6 +1328,7 @@ struct ComposeView_Previews: PreviewProvider {
.environmentObject(ChatModel())
ComposeView(
chat: chat,
+ im: im,
composeState: $composeState,
keyboardVisible: Binding.constant(true),
keyboardHiddenDate: Binding.constant(Date.now),
diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift
new file mode 100644
index 0000000000..96915b342f
--- /dev/null
+++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift
@@ -0,0 +1,108 @@
+//
+// 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("Reject")
+ .foregroundColor(.red)
+ }
+ .frame(maxWidth: .infinity)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ showRejectMemberAlert(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 showRejectMemberAlert(_ groupInfo: GroupInfo, _ member: GroupMember, dismiss: DismissAction? = nil) {
+ showAlert(
+ title: NSLocalizedString("Reject member?", comment: "alert title"),
+ buttonTitle: "Reject",
+ buttonAction: { removeMember(groupInfo, member, dismiss: dismiss) },
+ cancelButton: true
+ )
+}
+
+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(gInfo, 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
+ )
+}
diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift
index 7cd543af10..3154f16f5b 100644
--- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift
@@ -78,6 +78,12 @@ 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,
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift
index 15749b0761..4218e94224 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift
@@ -17,6 +17,7 @@ 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
@@ -87,7 +88,25 @@ 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()
@@ -96,19 +115,6 @@ struct GroupChatInfoView: View {
addOrEditWelcomeMessage()
}
GroupPreferencesButton(groupInfo: $groupInfo, preferences: groupInfo.fullGroupPreferences, currentPreferences: groupInfo.fullGroupPreferences)
- if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT {
- sendReceiptsOption()
- } else {
- sendReceiptsOptionDisabled()
- }
-
- NavigationLink {
- ChatWallpaperEditorSheet(chat: chat)
- } label: {
- Label("Chat theme", systemImage: "photo")
- }
- } header: {
- Text("")
} footer: {
let label: LocalizedStringKey = (
groupInfo.businessChat == nil
@@ -120,6 +126,16 @@ struct GroupChatInfoView: View {
}
Section {
+ if members.filter({ $0.wrapped.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT {
+ sendReceiptsOption()
+ } else {
+ sendReceiptsOptionDisabled()
+ }
+ NavigationLink {
+ ChatWallpaperEditorSheet(chat: chat)
+ } label: {
+ Label("Chat theme", systemImage: "photo")
+ }
ChatTTLOption(chat: chat, progressIndicator: $progressIndicator)
} footer: {
Text("Delete chat messages from your device.")
@@ -127,9 +143,6 @@ 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))
@@ -144,9 +157,16 @@ struct GroupChatInfoView: View {
let filteredMembers = s == ""
? members
: members.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) }
- MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: GMember(groupInfo.membership), user: true, alert: $alert)
+ MemberRowView(
+ chat: chat,
+ groupInfo: groupInfo,
+ groupMember: GMember(groupInfo.membership),
+ scrollToItemId: $scrollToItemId,
+ user: true,
+ alert: $alert
+ )
ForEach(filteredMembers) { member in
- MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: member, alert: $alert)
+ MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: member, scrollToItemId: $scrollToItemId, alert: $alert)
}
}
@@ -155,7 +175,7 @@ struct GroupChatInfoView: View {
if groupInfo.canDelete {
deleteGroupButton()
}
- if groupInfo.membership.memberCurrent {
+ if groupInfo.membership.memberCurrentOrPending {
leaveGroupButton()
}
}
@@ -353,6 +373,7 @@ 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?
@@ -415,7 +436,7 @@ struct GroupChatInfoView: View {
}
private func memberInfoView() -> some View {
- GroupMemberInfoView(groupInfo: groupInfo, chat: chat, groupMember: groupMember)
+ GroupMemberInfoView(groupInfo: groupInfo, chat: chat, groupMember: groupMember, scrollToItemId: $scrollToItemId)
.navigationBarHidden(false)
}
@@ -523,6 +544,89 @@ 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(
@@ -683,26 +787,36 @@ struct GroupChatInfoView: View {
title: Text("Remove member?"),
message: Text(messageLabel),
primaryButton: .destructive(Text("Remove")) {
- Task {
- do {
- let updatedMembers = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId])
- await MainActor.run {
- updatedMembers.forEach { updatedMember in
- _ = chatModel.upsertGroupMember(groupInfo, updatedMember)
- }
- }
- } catch let error {
- logger.error("apiRemoveMembers error: \(responseError(error))")
- let a = getErrorAlert(error, "Error removing member")
- alert = .error(title: a.title, error: a.message)
- }
- }
+ removeMember(groupInfo, mem)
},
secondaryButton: .cancel()
)
}
}
+func removeMember(_ groupInfo: GroupInfo, _ mem: GroupMember, 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!")
@@ -796,6 +910,7 @@ struct GroupChatInfoView_Previews: PreviewProvider {
GroupChatInfoView(
chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []),
groupInfo: Binding.constant(GroupInfo.sampleData),
+ scrollToItemId: Binding.constant(nil),
onSearch: {},
localAlias: ""
)
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
index 79ad242366..fa7fc7cae4 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift
@@ -16,6 +16,7 @@ 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
@@ -103,6 +104,10 @@ 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 {
@@ -278,7 +283,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
}
}
@@ -474,6 +479,31 @@ 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 {
@@ -610,10 +640,11 @@ struct GroupMemberInfoView: View {
primaryButton: .destructive(Text("Remove")) {
Task {
do {
- let updatedMembers = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId])
+ let (updatedGroupInfo, updatedMembers) = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId])
await MainActor.run {
+ chatModel.updateGroup(updatedGroupInfo)
updatedMembers.forEach { updatedMember in
- _ = chatModel.upsertGroupMember(groupInfo, updatedMember)
+ _ = chatModel.upsertGroupMember(updatedGroupInfo, updatedMember)
}
dismiss()
}
@@ -821,7 +852,8 @@ struct GroupMemberInfoView_Previews: PreviewProvider {
GroupMemberInfoView(
groupInfo: GroupInfo.sampleData,
chat: Chat.sampleData,
- groupMember: GMember.sampleData
+ groupMember: GMember.sampleData,
+ scrollToItemId: Binding.constant(nil)
)
}
}
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift
index 9bb4a0cc35..07cc7bd217 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupMentions.swift
@@ -17,6 +17,7 @@ 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
@@ -93,12 +94,31 @@ 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 s.isEmpty
- ? sortedMembers
- : sortedMembers.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) }
+ return sortedMembers.filter {
+ contextMemberFilter($0.wrapped)
+ && (s.isEmpty || $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s))
+ }
}
private func messageChanged(_ msg: String, _ parsedMsg: [FormattedText], _ range: NSRange) {
diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift
index ed39c401ce..55b1dc6d2e 100644
--- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift
+++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift
@@ -30,6 +30,14 @@ struct GroupPreferencesView: View {
let saveText: LocalizedStringKey = creatingGroup ? "Save" : "Save and notify group members"
VStack {
List {
+ Section {
+ MemberAdmissionButton(
+ groupInfo: $groupInfo,
+ admission: groupInfo.groupProfile.memberAdmission_,
+ currentAdmission: groupInfo.groupProfile.memberAdmission_,
+ creatingGroup: creatingGroup
+ )
+ }
featureSection(.timedMessages, $preferences.timedMessages.enable)
featureSection(.fullDelete, $preferences.fullDelete.enable)
featureSection(.directMessages, $preferences.directMessages.enable, $preferences.directMessages.role)
@@ -140,6 +148,66 @@ 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(
diff --git a/apps/ios/Shared/Views/Chat/Group/MemberAdmissionView.swift b/apps/ios/Shared/Views/Chat/Group/MemberAdmissionView.swift
new file mode 100644
index 0000000000..d80615b5d2
--- /dev/null
+++ b/apps/ios/Shared/Views/Chat/Group/MemberAdmissionView.swift
@@ -0,0 +1,93 @@
+//
+// 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) -> 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: {}
+ )
+}
diff --git a/apps/ios/Shared/Views/Chat/Group/MemberSupportChatToolbar.swift b/apps/ios/Shared/Views/Chat/Group/MemberSupportChatToolbar.swift
new file mode 100644
index 0000000000..23001e64bf
--- /dev/null
+++ b/apps/ios/Shared/Views/Chat/Group/MemberSupportChatToolbar.swift
@@ -0,0 +1,44 @@
+//
+// MemberSupportChatToolbar.swift
+// SimpleX (iOS)
+//
+// Created by spaced4ndy on 01.05.2025.
+// Copyright © 2025 SimpleX Chat. All rights reserved.
+//
+
+import SwiftUI
+import SimpleXChat
+
+struct MemberSupportChatToolbar: View {
+ @Environment(\.colorScheme) var colorScheme
+ @EnvironmentObject var theme: AppTheme
+ var groupMember: GroupMember
+ var imageSize: CGFloat = 32
+
+ var body: some View {
+ return HStack {
+ MemberProfileImage(groupMember, size: imageSize)
+ .padding(.trailing, 4)
+ let t = Text(groupMember.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())
+}
diff --git a/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift
new file mode 100644
index 0000000000..7f3672ea17
--- /dev/null
+++ b/apps/ios/Shared/Views/Chat/Group/MemberSupportView.swift
@@ -0,0 +1,287 @@
+//
+// 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)
+ } else {
+ Button {
+ showDeleteMemberSupportChatAlert(groupInfo, memberWithChat.wrapped)
+ } label: {
+ Label("Delete", 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 showDeleteMemberSupportChatAlert(_ groupInfo: GroupInfo, _ member: GroupMember) {
+ showAlert(
+ title: NSLocalizedString("Delete chat with member?", comment: "alert title"),
+ buttonTitle: "Delete",
+ buttonAction: { deleteMemberSupportChat(groupInfo, member) },
+ cancelButton: true
+ )
+}
+
+func deleteMemberSupportChat(_ groupInfo: GroupInfo, _ member: GroupMember) {
+ Task {
+ do {
+ let (gInfo, updatedMember) = try await apiDeleteMemberSupportChat(groupInfo.groupId, member.groupMemberId)
+ await MainActor.run {
+ _ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember)
+ ChatModel.shared.updateGroup(gInfo)
+ }
+ // TODO member row doesn't get removed from list (upsertGroupMember correctly sets supportChat to nil) - this repopulates list to fix it
+ await ChatModel.shared.loadGroupMembers(gInfo)
+ } catch let error {
+ logger.error("apiDeleteMemberSupportChat error: \(responseError(error))")
+ await MainActor.run {
+ showAlert(
+ NSLocalizedString("Error deleting chat with member", comment: "alert title"),
+ message: responseError(error)
+ )
+ }
+ }
+ }
+}
+
+#Preview {
+ MemberSupportView(
+ groupInfo: GroupInfo.sampleData,
+ scrollToItemId: Binding.constant(nil)
+ )
+}
diff --git a/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift b/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift
new file mode 100644
index 0000000000..47c5df264f
--- /dev/null
+++ b/apps/ios/Shared/Views/Chat/Group/SecondaryChatView.swift
@@ -0,0 +1,42 @@
+//
+// 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)
+ )
+}
diff --git a/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift b/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift
index 85d6b279c5..e397970acd 100644
--- a/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift
+++ b/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift
@@ -25,7 +25,7 @@ struct SelectedItemsTopToolbar: View {
struct SelectedItemsBottomToolbar: View {
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
- let chatItems: [ChatItem]
+ let im: ItemsModel
@Binding var selectedChatItems: Set?
var chatInfo: ChatInfo
// Bool - delete for everyone is possible
@@ -75,9 +75,9 @@ struct SelectedItemsBottomToolbar: View {
.resizable()
.scaledToFit()
.frame(width: 20, height: 20, alignment: .center)
- .foregroundColor(!moderateEnabled || deleteCountProhibited ? theme.colors.secondary : .red)
+ .foregroundColor(!moderateEnabled || deleteCountProhibited || im.secondaryIMFilter != nil ? theme.colors.secondary : .red)
}
- .disabled(!moderateEnabled || deleteCountProhibited)
+ .disabled(!moderateEnabled || deleteCountProhibited || im.secondaryIMFilter != nil)
.opacity(canModerate ? 1 : 0)
Spacer()
@@ -88,24 +88,24 @@ struct SelectedItemsBottomToolbar: View {
.resizable()
.scaledToFit()
.frame(width: 20, height: 20, alignment: .center)
- .foregroundColor(!forwardEnabled || forwardCountProhibited ? theme.colors.secondary : theme.colors.primary)
+ .foregroundColor(!forwardEnabled || forwardCountProhibited || im.secondaryIMFilter != nil ? theme.colors.secondary : theme.colors.primary)
}
- .disabled(!forwardEnabled || forwardCountProhibited)
+ .disabled(!forwardEnabled || forwardCountProhibited || im.secondaryIMFilter != nil)
}
.frame(maxHeight: .infinity)
.padding([.leading, .trailing], 12)
}
.onAppear {
- recheckItems(chatInfo, chatItems, selectedChatItems)
+ recheckItems(chatInfo, im.reversedChatItems, selectedChatItems)
}
.onChange(of: chatInfo) { info in
- recheckItems(info, chatItems, selectedChatItems)
+ recheckItems(info, im.reversedChatItems, selectedChatItems)
}
- .onChange(of: chatItems) { items in
+ .onChange(of: im.reversedChatItems) { items in
recheckItems(chatInfo, items, selectedChatItems)
}
.onChange(of: selectedChatItems) { selected in
- recheckItems(chatInfo, chatItems, selected)
+ recheckItems(chatInfo, im.reversedChatItems, selected)
}
.frame(height: 55.5)
.background(.thinMaterial)
@@ -116,7 +116,7 @@ struct SelectedItemsBottomToolbar: View {
deleteCountProhibited = count == 0 || count > 200
forwardCountProhibited = count == 0 || count > 20
canModerate = possibleToModerate(chatInfo)
- let groupInfo: GroupInfo? = if case let ChatInfo.group(groupInfo: info) = chatInfo {
+ let groupInfo: GroupInfo? = if case let ChatInfo.group(groupInfo: info, _) = chatInfo {
info
} else {
nil
@@ -145,7 +145,7 @@ struct SelectedItemsBottomToolbar: View {
private func possibleToModerate(_ chatInfo: ChatInfo) -> Bool {
return switch chatInfo {
- case let .group(groupInfo):
+ case let .group(groupInfo, _):
groupInfo.membership.memberRole >= .admin
default: false
}
diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift
index 81d78fbadd..1e747b8019 100644
--- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift
@@ -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.memberCurrent) {
+ if (groupInfo.membership.memberCurrentOrPending) {
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.memberCurrent
+ let showLeaveGroup = groupInfo.membership.memberCurrentOrPending
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(chat) }
+ Task { await markChatRead(ItemsModel.shared, chat) }
} label: {
SwipeLabel(NSLocalizedString("Read", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI)
}
@@ -482,12 +482,10 @@ struct ChatListNavLink: View {
.tint(theme.colors.primary)
}
.appSheet(isPresented: $showContactConnectionInfo) {
- Group {
- if case let .contactConnection(contactConnection) = chat.chatInfo {
- ContactConnectionInfo(contactConnection: contactConnection)
- .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil)
- .modifier(ThemedBackground(grouped: true))
- }
+ if case let .contactConnection(contactConnection) = chat.chatInfo {
+ ContactConnectionInfo(contactConnection: contactConnection)
+ .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil)
+ .modifier(ThemedBackground(grouped: true))
}
}
.contentShape(Rectangle())
diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift
index f34f930c6f..377764ac83 100644
--- a/apps/ios/Shared/Views/ChatList/ChatListView.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift
@@ -148,6 +148,7 @@ struct ChatListView: View {
@State private var userPickerShown: Bool = false
@State private var sheet: SomeSheet? = 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
@@ -446,7 +447,14 @@ struct ChatListView: View {
@ViewBuilder private func chatView() -> some View {
if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) {
- ChatView(chat: chat)
+ let im = ItemsModel.shared
+ ChatView(
+ chat: chat,
+ im: im,
+ mergedItems: BoxedValue(MergedItems.create(im, [])),
+ floatingButtonModel: FloatingButtonModel(im: im),
+ scrollToItemId: $scrollToItemId
+ )
}
}
@@ -892,12 +900,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:
diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift
index b8c8233e6e..49f629d084 100644
--- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift
+++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift
@@ -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,11 +341,12 @@ 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()
@@ -439,7 +440,11 @@ struct ChatPreviewView: View {
if progressByTimeout {
ProgressView()
} else if chat.chatStats.reportsCount > 0 {
- groupReportsIcon(size: size * 0.8)
+ 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)
} else {
incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size)
}
@@ -485,12 +490,12 @@ struct ChatPreviewView: View {
}
}
-func groupReportsIcon(size: CGFloat) -> some View {
+func flagIcon(size: CGFloat, color: Color) -> some View {
Image(systemName: "flag")
.resizable()
.scaledToFit()
.frame(width: size, height: size)
- .foregroundColor(.red)
+ .foregroundColor(color)
}
func smallContentPreview(size: CGFloat, _ view: @escaping () -> some View) -> some View {
diff --git a/apps/ios/Shared/Views/ChatList/UserPicker.swift b/apps/ios/Shared/Views/ChatList/UserPicker.swift
index dbe10ad997..c38ddfb1da 100644
--- a/apps/ios/Shared/Views/ChatList/UserPicker.swift
+++ b/apps/ios/Shared/Views/ChatList/UserPicker.swift
@@ -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) {
- UnreadBadge(userInfo: u).offset(x: 4, y: -4)
+ userUnreadBadge(u, theme: theme).offset(x: 4, y: -4)
}
}
.padding(.trailing, 6)
@@ -171,19 +171,27 @@ 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(userInfo.unreadCount)
+ unreadCountText(count)
.font(userFont <= .xxxLarge ? .caption : .caption2)
.foregroundColor(.white)
.padding(.horizontal, dynamicSize(userFont).unreadPadding)
.frame(minWidth: size, minHeight: size)
- .background(userInfo.user.showNtfs ? theme.colors.primary : theme.colors.secondary)
+ .background(color)
.cornerRadius(dynamicSize(userFont).unreadCorner)
}
}
diff --git a/apps/ios/Shared/Views/Helpers/AppSheet.swift b/apps/ios/Shared/Views/Helpers/AppSheet.swift
index 1e334367e8..17fe95a058 100644
--- a/apps/ios/Shared/Views/Helpers/AppSheet.swift
+++ b/apps/ios/Shared/Views/Helpers/AppSheet.swift
@@ -33,7 +33,7 @@ extension View {
func appSheet(
isPresented: Binding,
onDismiss: (() -> Void)? = nil,
- content: @escaping () -> Content
+ @ViewBuilder content: @escaping () -> Content
) -> some View where Content: View {
sheet(isPresented: isPresented, onDismiss: onDismiss) {
content().modifier(PrivacySensitive())
@@ -43,7 +43,7 @@ extension View {
func appSheet(
item: Binding,
onDismiss: (() -> Void)? = nil,
- content: @escaping (T) -> Content
+ @ViewBuilder content: @escaping (T) -> Content
) -> some View where T: Identifiable, Content: View {
sheet(item: item, onDismiss: onDismiss) { it in
content(it).modifier(PrivacySensitive())
diff --git a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift
index 16ab26eff7..c21ff9be8b 100644
--- a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift
+++ b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift
@@ -66,6 +66,8 @@ struct LocalAuthView: View {
m.chatId = nil
ItemsModel.shared.reversedChatItems = []
ItemsModel.shared.chatState.clear()
+ ChatModel.shared.secondaryIM?.reversedChatItems = []
+ ChatModel.shared.secondaryIM?.chatState.clear()
m.updateChats([])
m.users = []
_ = kcAppPassword.set(password)
diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift
index 87c0b80372..b3c33e95ea 100644
--- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift
+++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift
@@ -193,7 +193,7 @@ struct AddGroupView: View {
Task {
await m.loadGroupMembers(gInfo)
}
- let c = Chat(chatInfo: .group(groupInfo: gInfo), chatItems: [])
+ let c = Chat(chatInfo: .group(groupInfo: gInfo, groupChatScope: nil), chatItems: [])
m.addChat(c)
withAnimation {
groupInfo = gInfo
diff --git a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift
index c6d0e27289..02dec5a618 100644
--- a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift
+++ b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift
@@ -367,13 +367,13 @@ struct ChatThemePreview: View {
let alice = ChatItem.getSample(1, CIDirection.directRcv, Date.now, NSLocalizedString("Good afternoon!", comment: "message preview"))
let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir))
HStack {
- ChatItemView(chat: Chat.sampleData, chatItem: alice, scrollToItemId: { _ in })
+ ChatItemView(chat: Chat.sampleData, im: ItemsModel.shared, chatItem: alice, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
.modifier(ChatItemClipped(alice, tailVisible: true))
Spacer()
}
HStack {
Spacer()
- ChatItemView(chat: Chat.sampleData, chatItem: bob, scrollToItemId: { _ in })
+ ChatItemView(chat: Chat.sampleData, im: ItemsModel.shared, chatItem: bob, scrollToItem: { _ in }, scrollToItemId: Binding.constant(nil))
.modifier(ChatItemClipped(bob, tailVisible: true))
.frame(alignment: .trailing)
}
diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
index e06b1c4dd3..50a012f4f8 100644
--- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift
+++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift
@@ -58,6 +58,7 @@ let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab"
let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown"
let DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE = "showHiddenProfilesNotice"
let DEFAULT_SHOW_MUTE_PROFILE_ALERT = "showMuteProfileAlert"
+let DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT = "showReportsInSupportChatAlert"
let DEFAULT_WHATS_NEW_VERSION = "defaultWhatsNewVersion"
let DEFAULT_ONBOARDING_STAGE = "onboardingStage"
let DEFAULT_MIGRATION_TO_STAGE = "migrationToStage"
@@ -117,6 +118,7 @@ let appDefaults: [String: Any] = [
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false,
DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE: true,
DEFAULT_SHOW_MUTE_PROFILE_ALERT: true,
+ DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT: true,
DEFAULT_ONBOARDING_STAGE: OnboardingStage.onboardingComplete.rawValue,
DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME: 300,
DEFAULT_SHOW_UNREAD_AND_FAVORITES: false,
@@ -144,6 +146,7 @@ let hintDefaults = [
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN,
DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE,
DEFAULT_SHOW_MUTE_PROFILE_ALERT,
+ DEFAULT_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT,
DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE,
DEFAULT_SHOW_DELETE_CONTACT_NOTICE
]
@@ -195,6 +198,8 @@ 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)
diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
index 887023b670..ddfe59e719 100644
--- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
+++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
@@ -350,7 +350,7 @@ struct UserProfilesView: View {
Image(systemName: "checkmark").foregroundColor(theme.colors.onBackground)
} else {
if userInfo.unreadCount > 0 {
- UnreadBadge(userInfo: userInfo)
+ userUnreadBadge(userInfo, theme: theme)
}
if user.hidden {
Image(systemName: "lock").foregroundColor(theme.colors.secondary)
diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff
index 776199ac1f..b5a217d8c0 100644
--- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff
+++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff
@@ -554,6 +554,14 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ alert action
+
+
+ Accept as observer
+ alert action
+
Accept conditions
Приеми условията
@@ -575,6 +583,10 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ alert title
+
Accepted conditions
Приети условия
@@ -1535,11 +1547,23 @@ set passcode view
Chat will be deleted for you - this cannot be undone!
No comment provided by engineer.
+
+ Chat with admins
+ chat toolbar
+
+
+ Chat with member
+ No comment provided by engineer.
+
Chats
Чатове
No comment provided by engineer.
+
+ Chats with members
+ No comment provided by engineer.
+
Check messages every 20 min.
No comment provided by engineer.
@@ -2300,6 +2324,10 @@ swipe action
Изтриване на чат профила?
No comment provided by engineer.
+
+ Delete chat with member?
+ alert title
+
Delete chat?
No comment provided by engineer.
@@ -2698,7 +2726,7 @@ swipe action
Don't show again
Не показвай отново
- No comment provided by engineer.
+ alert action
Done
@@ -3006,6 +3034,10 @@ chat item action
Грешка при приемане на заявка за контакт
No comment provided by engineer.
+
+ Error accepting member
+ alert title
+
Error adding member(s)
Грешка при добавяне на член(ове)
@@ -3094,6 +3126,10 @@ chat item action
Грешка при изтриване на базата данни
No comment provided by engineer.
+
+ Error deleting chat with member
+ alert title
+
Error deleting chat!
Грешка при изтриването на чата!
@@ -3196,7 +3232,7 @@ chat item action
Error removing member
Грешка при отстраняване на член
- No comment provided by engineer.
+ alert title
Error reordering lists
@@ -4508,6 +4544,10 @@ This is your link for group %@!
Член
No comment provided by engineer.
+
+ Member admission
+ No comment provided by engineer.
+
Member inactive
item status text
@@ -4539,6 +4579,10 @@ This is your link for group %@!
Членът ще бъде премахнат от групата - това не може да бъде отменено!
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ alert message
+
Members can add message reactions.
Членовете на групата могат да добавят реакции към съобщенията.
@@ -4931,6 +4975,10 @@ This is your link for group %@!
Нова членска роля
No comment provided by engineer.
+
+ New member wants to join the group.
+ rcv group event chat item
+
New message
Ново съобщение
@@ -4967,6 +5015,10 @@ This is your link for group %@!
No chats in list %@
No comment provided by engineer.
+
+ No chats with members
+ No comment provided by engineer.
+
No contacts selected
Няма избрани контакти
@@ -5140,7 +5192,8 @@ This is your link for group %@!
Ok
Ок
- alert button
+ alert action
+alert button
Old database
@@ -5531,6 +5584,10 @@ Error: %@
Please try to disable and re-enable notfications.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ snd group event chat item
+
Please wait for token activation to complete.
token info
@@ -5950,6 +6007,10 @@ swipe action
Отхвърли заявката за контакт
No comment provided by engineer.
+
+ Reject member?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
Реле сървър се използва само ако е необходимо. Друга страна може да наблюдава вашия IP адрес.
@@ -6053,6 +6114,10 @@ swipe action
Report reason?
No comment provided by engineer.
+
+ Report sent to moderators
+ alert title
+
Report spam: only group moderators will see it.
report reason
@@ -6157,6 +6222,14 @@ swipe action
Review conditions
No comment provided by engineer.
+
+ Review members
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ admission stage description
+
Revoke
Отзови
@@ -6210,6 +6283,10 @@ chat item action
Запази (и уведоми контактите)
alert button
+
+ Save admission settings?
+ alert title
+
Save and notify contact
Запази и уведоми контакта
@@ -6689,6 +6766,10 @@ chat item action
Задайте го вместо системната идентификация.
No comment provided by engineer.
+
+ Set member admission
+ No comment provided by engineer.
+
Set message expiration in chats.
No comment provided by engineer.
@@ -8343,6 +8424,10 @@ Repeat join request?
Можете да видите отново линкът за покана в подробностите за връзката.
alert message
+
+ You can view your reports in Chat with admins.
+ alert message
+
You can't send messages!
Не може да изпращате съобщения!
@@ -8635,6 +8720,10 @@ Repeat connection request?
по-горе, след това избери:
No comment provided by engineer.
+
+ accepted %@
+ rcv group event chat item
+
accepted call
обаждането прието
@@ -8644,6 +8733,10 @@ Repeat connection request?
accepted invitation
chat list item title
+
+ accepted you
+ rcv group event chat item
+
admin
админ
@@ -8664,6 +8757,10 @@ Repeat connection request?
съгласуване на криптиране…
chat item text
+
+ all
+ member criteria value
+
all members
всички членове
@@ -8747,6 +8844,10 @@ marked deleted chat item preview text
повикване…
call status
+
+ can't send messages
+ No comment provided by engineer.
+
cancelled %@
отменен %@
@@ -8852,6 +8953,14 @@ marked deleted chat item preview text
името на контакта %1$@ е променено на %2$@
profile update event chat item
+
+ contact deleted
+ No comment provided by engineer.
+
+
+ contact disabled
+ No comment provided by engineer.
+
contact has e2e encryption
контактът има e2e криптиране
@@ -8862,6 +8971,10 @@ marked deleted chat item preview text
контактът няма e2e криптиране
No comment provided by engineer.
+
+ contact not ready
+ No comment provided by engineer.
+
creator
създател
@@ -9030,6 +9143,10 @@ pref value
групата е изтрита
No comment provided by engineer.
+
+ group is deleted
+ No comment provided by engineer.
+
group profile updated
профилът на групата е актуализиран
@@ -9153,6 +9270,10 @@ pref value
свързан
rcv group event chat item
+
+ member has old version
+ No comment provided by engineer.
+
message
No comment provided by engineer.
@@ -9216,6 +9337,10 @@ pref value
няма текст
copied message info in history
+
+ not synchronized
+ No comment provided by engineer.
+
observer
наблюдател
@@ -9226,6 +9351,7 @@ pref value
изключено
enabled status
group pref value
+member criteria value
time to disappear
@@ -9274,6 +9400,10 @@ time to disappear
pending approval
No comment provided by engineer.
+
+ pending review
+ No comment provided by engineer.
+
quantum resistant e2e encryption
квантово устойчиво e2e криптиране
@@ -9313,6 +9443,10 @@ time to disappear
премахнат адрес за контакт
profile update event chat item
+
+ removed from group
+ No comment provided by engineer.
+
removed profile picture
премахната профилна снимка
@@ -9323,10 +9457,22 @@ time to disappear
ви острани
rcv group event chat item
+
+ request to join rejected
+ No comment provided by engineer.
+
requested to connect
chat list item title
+
+ review
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ No comment provided by engineer.
+
saved
запазено
@@ -9508,6 +9654,10 @@ last received msg: %2$@
вие
No comment provided by engineer.
+
+ you accepted this member
+ snd group event chat item
+
you are invited to group
вие сте поканени в групата
diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff
index 0400839cb0..fe0e02ccdf 100644
--- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff
+++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff
@@ -544,6 +544,14 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ alert action
+
+
+ Accept as observer
+ alert action
+
Accept conditions
No comment provided by engineer.
@@ -564,6 +572,10 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ alert title
+
Accepted conditions
No comment provided by engineer.
@@ -1456,11 +1468,23 @@ set passcode view
Chat will be deleted for you - this cannot be undone!
No comment provided by engineer.
+
+ Chat with admins
+ chat toolbar
+
+
+ Chat with member
+ No comment provided by engineer.
+
Chats
Chaty
No comment provided by engineer.
+
+ Chats with members
+ No comment provided by engineer.
+
Check messages every 20 min.
No comment provided by engineer.
@@ -2193,6 +2217,10 @@ swipe action
Smazat chat profil?
No comment provided by engineer.
+
+ Delete chat with member?
+ alert title
+
Delete chat?
No comment provided by engineer.
@@ -2584,7 +2612,7 @@ swipe action
Don't show again
Znovu neukazuj
- No comment provided by engineer.
+ alert action
Done
@@ -2878,6 +2906,10 @@ chat item action
Chyba při přijímání žádosti o kontakt
No comment provided by engineer.
+
+ Error accepting member
+ alert title
+
Error adding member(s)
Chyba přidávání člena(ů)
@@ -2965,6 +2997,10 @@ chat item action
Chyba při mazání databáze chatu
No comment provided by engineer.
+
+ Error deleting chat with member
+ alert title
+
Error deleting chat!
Chyba při mazání chatu!
@@ -3065,7 +3101,7 @@ chat item action
Error removing member
Chyba při odebrání člena
- No comment provided by engineer.
+ alert title
Error reordering lists
@@ -4333,6 +4369,10 @@ This is your link for group %@!
Člen
No comment provided by engineer.
+
+ Member admission
+ No comment provided by engineer.
+
Member inactive
item status text
@@ -4364,6 +4404,10 @@ This is your link for group %@!
Člen bude odstraněn ze skupiny - toto nelze vzít zpět!
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ alert message
+
Members can add message reactions.
Členové skupin mohou přidávat reakce na zprávy.
@@ -4739,6 +4783,10 @@ This is your link for group %@!
Nová role člena
No comment provided by engineer.
+
+ New member wants to join the group.
+ rcv group event chat item
+
New message
Nová zpráva
@@ -4775,6 +4823,10 @@ This is your link for group %@!
No chats in list %@
No comment provided by engineer.
+
+ No chats with members
+ No comment provided by engineer.
+
No contacts selected
Nebyl vybrán žádný kontakt
@@ -4945,7 +4997,8 @@ This is your link for group %@!
Ok
Ok
- alert button
+ alert action
+alert button
Old database
@@ -5320,6 +5373,10 @@ Error: %@
Please try to disable and re-enable notfications.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ snd group event chat item
+
Please wait for token activation to complete.
token info
@@ -5731,6 +5788,10 @@ swipe action
Odmítnout žádost o kontakt
No comment provided by engineer.
+
+ Reject member?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
Přenosový server se používá pouze v případě potřeby. Jiná strana může sledovat vaši IP adresu.
@@ -5829,6 +5890,10 @@ swipe action
Report reason?
No comment provided by engineer.
+
+ Report sent to moderators
+ alert title
+
Report spam: only group moderators will see it.
report reason
@@ -5932,6 +5997,14 @@ swipe action
Review conditions
No comment provided by engineer.
+
+ Review members
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ admission stage description
+
Revoke
Odvolat
@@ -5984,6 +6057,10 @@ chat item action
Uložit (a informovat kontakty)
alert button
+
+ Save admission settings?
+ alert title
+
Save and notify contact
Uložit a upozornit kontakt
@@ -6455,6 +6532,10 @@ chat item action
Nastavte jej namísto ověřování systému.
No comment provided by engineer.
+
+ Set member admission
+ No comment provided by engineer.
+
Set message expiration in chats.
No comment provided by engineer.
@@ -8044,6 +8125,10 @@ Repeat join request?
You can view invitation link again in connection details.
alert message
+
+ You can view your reports in Chat with admins.
+ alert message
+
You can't send messages!
Nemůžete posílat zprávy!
@@ -8330,6 +8415,10 @@ Repeat connection request?
výše, pak vyberte:
No comment provided by engineer.
+
+ accepted %@
+ rcv group event chat item
+
accepted call
přijatý hovor
@@ -8339,6 +8428,10 @@ Repeat connection request?
accepted invitation
chat list item title
+
+ accepted you
+ rcv group event chat item
+
admin
správce
@@ -8358,6 +8451,10 @@ Repeat connection request?
povoluji šifrování…
chat item text
+
+ all
+ member criteria value
+
all members
feature role
@@ -8435,6 +8532,10 @@ marked deleted chat item preview text
volání…
call status
+
+ can't send messages
+ No comment provided by engineer.
+
cancelled %@
zrušeno %@
@@ -8539,6 +8640,14 @@ marked deleted chat item preview text
contact %1$@ changed to %2$@
profile update event chat item
+
+ contact deleted
+ No comment provided by engineer.
+
+
+ contact disabled
+ No comment provided by engineer.
+
contact has e2e encryption
kontakt má šifrování e2e
@@ -8549,6 +8658,10 @@ marked deleted chat item preview text
kontakt nemá šifrování e2e
No comment provided by engineer.
+
+ contact not ready
+ No comment provided by engineer.
+
creator
tvůrce
@@ -8715,6 +8828,10 @@ pref value
skupina smazána
No comment provided by engineer.
+
+ group is deleted
+ No comment provided by engineer.
+
group profile updated
profil skupiny aktualizován
@@ -8837,6 +8954,10 @@ pref value
připojeno
rcv group event chat item
+
+ member has old version
+ No comment provided by engineer.
+
message
No comment provided by engineer.
@@ -8900,6 +9021,10 @@ pref value
žádný text
copied message info in history
+
+ not synchronized
+ No comment provided by engineer.
+
observer
pozorovatel
@@ -8910,6 +9035,7 @@ pref value
vypnuto
enabled status
group pref value
+member criteria value
time to disappear
@@ -8957,6 +9083,10 @@ time to disappear
pending approval
No comment provided by engineer.
+
+ pending review
+ No comment provided by engineer.
+
quantum resistant e2e encryption
chat item text
@@ -8994,6 +9124,10 @@ time to disappear
removed contact address
profile update event chat item
+
+ removed from group
+ No comment provided by engineer.
+
removed profile picture
profile update event chat item
@@ -9003,10 +9137,22 @@ time to disappear
odstranil vás
rcv group event chat item
+
+ request to join rejected
+ No comment provided by engineer.
+
requested to connect
chat list item title
+
+ review
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ No comment provided by engineer.
+
saved
No comment provided by engineer.
@@ -9178,6 +9324,10 @@ last received msg: %2$@
you
No comment provided by engineer.
+
+ you accepted this member
+ snd group event chat item
+
you are invited to group
jste pozváni do skupiny
diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff
index 06fd7c5a1d..b2e404c141 100644
--- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff
+++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff
@@ -565,6 +565,16 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ Als Mitglied übernehmen
+ alert action
+
+
+ Accept as observer
+ Als Beobachter übernehmen
+ alert action
+
Accept conditions
Nutzungsbedingungen akzeptieren
@@ -586,6 +596,11 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ Mitglied übernehmen
+ alert title
+
Accepted conditions
Akzeptierte Nutzungsbedingungen
@@ -1582,11 +1597,26 @@ set passcode view
Der Chat wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!
No comment provided by engineer.
+
+ Chat with admins
+ Chat mit Administratoren
+ chat toolbar
+
+
+ Chat with member
+ Chat mit einem Mitglied
+ No comment provided by engineer.
+
Chats
Chats
No comment provided by engineer.
+
+ Chats with members
+ Chats mit Mitgliedern
+ No comment provided by engineer.
+
Check messages every 20 min.
Alle 20min Nachrichten überprüfen.
@@ -2402,6 +2432,11 @@ swipe action
Chat-Profil löschen?
No comment provided by engineer.
+
+ Delete chat with member?
+ Chat mit dem Mitglied löschen?
+ alert title
+
Delete chat?
Chat löschen?
@@ -2825,7 +2860,7 @@ swipe action
Don't show again
Nicht nochmals anzeigen
- No comment provided by engineer.
+ alert action
Done
@@ -3143,6 +3178,11 @@ chat item action
Fehler beim Annehmen der Kontaktanfrage
No comment provided by engineer.
+
+ Error accepting member
+ Fehler beim Übernehmen des Mitglieds
+ alert title
+
Error adding member(s)
Fehler beim Hinzufügen von Mitgliedern
@@ -3238,6 +3278,11 @@ chat item action
Fehler beim Löschen der Chat-Datenbank
No comment provided by engineer.
+
+ Error deleting chat with member
+ Fehler beim Löschen des Chats mit dem Mitglied
+ alert title
+
Error deleting chat!
Fehler beim Löschen des Chats!
@@ -3346,7 +3391,7 @@ chat item action
Error removing member
Fehler beim Entfernen des Mitglieds
- No comment provided by engineer.
+ alert title
Error reordering lists
@@ -4730,6 +4775,11 @@ Das ist Ihr Link für die Gruppe %@!
Mitglied
No comment provided by engineer.
+
+ Member admission
+ Aufnahme von Mitgliedern
+ No comment provided by engineer.
+
Member inactive
Mitglied inaktiv
@@ -4765,6 +4815,11 @@ Das ist Ihr Link für die Gruppe %@!
Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden!
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ Ein Mitglied wird der Gruppe beitreten. Übernehmen?
+ alert message
+
Members can add message reactions.
Gruppenmitglieder können eine Reaktion auf Nachrichten geben.
@@ -5185,6 +5240,11 @@ Das ist Ihr Link für die Gruppe %@!
Neue Mitgliedsrolle
No comment provided by engineer.
+
+ New member wants to join the group.
+ Ein neues Mitglied will der Gruppe beitreten.
+ rcv group event chat item
+
New message
Neue Nachricht
@@ -5225,6 +5285,11 @@ Das ist Ihr Link für die Gruppe %@!
Keine Chats in der Liste %@
No comment provided by engineer.
+
+ No chats with members
+ Keine Chats mit Mitgliedern
+ No comment provided by engineer.
+
No contacts selected
Keine Kontakte ausgewählt
@@ -5417,7 +5482,8 @@ Das ist Ihr Link für die Gruppe %@!
Ok
Ok
- alert button
+ alert action
+alert button
Old database
@@ -5575,6 +5641,7 @@ Dies erfordert die Aktivierung eines VPNs.
Open link?
+ Link öffnen?
alert title
@@ -5828,6 +5895,11 @@ Fehler: %@
Bitte versuchen Sie, die Benachrichtigungen zu deaktivieren und wieder zu aktivieren.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ Bitte warten Sie auf die Überprüfung Ihrer Anfrage durch die Gruppen-Moderatoren, um der Gruppe beitreten zu können.
+ snd group event chat item
+
Please wait for token activation to complete.
Bitte warten Sie, bis die Token-Aktivierung abgeschlossen ist.
@@ -6281,6 +6353,11 @@ swipe action
Kontaktanfrage ablehnen
No comment provided by engineer.
+
+ Reject member?
+ Mitglied ablehnen?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
Relais-Server werden nur genutzt, wenn sie benötigt werden. Ihre IP-Adresse kann von Anderen erfasst werden.
@@ -6391,6 +6468,11 @@ swipe action
Grund der Meldung?
No comment provided by engineer.
+
+ Report sent to moderators
+ Meldung wurde an die Moderatoren gesendet
+ alert title
+
Report spam: only group moderators will see it.
Spam melden: Nur Gruppenmoderatoren werden es sehen.
@@ -6506,6 +6588,16 @@ swipe action
Nutzungsbedingungen einsehen
No comment provided by engineer.
+
+ Review members
+ Überprüfung der Mitglieder
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ Überprüfung der Mitglieder vor der Aufnahme ("Anklopfen").
+ admission stage description
+
Revoke
Widerrufen
@@ -6562,6 +6654,11 @@ chat item action
Speichern (und Kontakte benachrichtigen)
alert button
+
+ Save admission settings?
+ Speichern der Aufnahme-Einstellungen?
+ alert title
+
Save and notify contact
Speichern und Kontakt benachrichtigen
@@ -7077,6 +7174,11 @@ chat item action
Anstelle der System-Authentifizierung festlegen.
No comment provided by engineer.
+
+ Set member admission
+ Aufnahme von Mitgliedern festlegen
+ No comment provided by engineer.
+
Set message expiration in chats.
Verfallsdatum von Nachrichten in Chats festlegen.
@@ -8839,6 +8941,11 @@ Verbindungsanfrage wiederholen?
Den Einladungslink können Sie in den Details der Verbindung nochmals sehen.
alert message
+
+ You can view your reports in Chat with admins.
+ Sie können Ihre Meldungen im Chat mit den Administratoren sehen.
+ alert message
+
You can't send messages!
Sie können keine Nachrichten versenden!
@@ -9078,7 +9185,7 @@ Verbindungsanfrage wiederholen?
Your profile is stored on your device and only shared with your contacts.
- Das Profil wird nur mit Ihren Kontakten geteilt.
+ Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt.
No comment provided by engineer.
@@ -9141,6 +9248,11 @@ Verbindungsanfrage wiederholen?
Danach die gewünschte Aktion auswählen:
No comment provided by engineer.
+
+ accepted %@
+ %@ übernommen
+ rcv group event chat item
+
accepted call
Anruf angenommen
@@ -9151,6 +9263,11 @@ Verbindungsanfrage wiederholen?
Einladung angenommen
chat list item title
+
+ accepted you
+ hat Sie übernommen
+ rcv group event chat item
+
admin
Admin
@@ -9171,6 +9288,11 @@ Verbindungsanfrage wiederholen?
Verschlüsselung zustimmen…
chat item text
+
+ all
+ alle
+ member criteria value
+
all members
Alle Mitglieder
@@ -9257,6 +9379,11 @@ marked deleted chat item preview text
Anrufen…
call status
+
+ can't send messages
+ Es können keine Nachrichten gesendet werden
+ No comment provided by engineer.
+
cancelled %@
abgebrochen %@
@@ -9362,6 +9489,16 @@ marked deleted chat item preview text
Der Kontaktname wurde von %1$@ auf %2$@ geändert
profile update event chat item
+
+ contact deleted
+ Kontakt gelöscht
+ No comment provided by engineer.
+
+
+ contact disabled
+ Kontakt deaktiviert
+ No comment provided by engineer.
+
contact has e2e encryption
Kontakt nutzt E2E-Verschlüsselung
@@ -9372,6 +9509,11 @@ marked deleted chat item preview text
Kontakt nutzt keine E2E-Verschlüsselung
No comment provided by engineer.
+
+ contact not ready
+ Kontakt nicht bereit
+ No comment provided by engineer.
+
creator
Ersteller
@@ -9543,6 +9685,11 @@ pref value
Gruppe gelöscht
No comment provided by engineer.
+
+ group is deleted
+ Gruppe wird gelöscht
+ No comment provided by engineer.
+
group profile updated
Gruppenprofil aktualisiert
@@ -9668,6 +9815,11 @@ pref value
ist der Gruppe beigetreten
rcv group event chat item
+
+ member has old version
+ Das Mitglied hat eine alte App-Version
+ No comment provided by engineer.
+
message
Nachricht
@@ -9733,6 +9885,11 @@ pref value
Kein Text
copied message info in history
+
+ not synchronized
+ Nicht synchronisiert
+ No comment provided by engineer.
+
observer
Beobachter
@@ -9743,6 +9900,7 @@ pref value
Aus
enabled status
group pref value
+member criteria value
time to disappear
@@ -9795,6 +9953,11 @@ time to disappear
ausstehende Genehmigung
No comment provided by engineer.
+
+ pending review
+ Ausstehende Überprüfung
+ No comment provided by engineer.
+
quantum resistant e2e encryption
Quantum-resistente E2E-Verschlüsselung
@@ -9835,6 +9998,11 @@ time to disappear
Die Kontaktadresse wurde entfernt
profile update event chat item
+
+ removed from group
+ Von der Gruppe entfernt
+ No comment provided by engineer.
+
removed profile picture
Das Profil-Bild wurde entfernt
@@ -9845,11 +10013,26 @@ time to disappear
hat Sie aus der Gruppe entfernt
rcv group event chat item
+
+ request to join rejected
+ Beitrittsanfrage abgelehnt
+ No comment provided by engineer.
+
requested to connect
Zur Verbindung aufgefordert
chat list item title
+
+ review
+ Überprüfung
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ Von Administratoren überprüft
+ No comment provided by engineer.
+
saved
abgespeichert
@@ -10039,6 +10222,11 @@ Zuletzt empfangene Nachricht: %2$@
Profil
No comment provided by engineer.
+
+ you accepted this member
+ Sie haben dieses Mitglied übernommen
+ snd group event chat item
+
you are invited to group
Sie sind zu der Gruppe eingeladen
diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff
index fd71e0dee6..5982f620b8 100644
--- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff
+++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff
@@ -565,6 +565,16 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ Accept as member
+ alert action
+
+
+ Accept as observer
+ Accept as observer
+ alert action
+
Accept conditions
Accept conditions
@@ -586,6 +596,11 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ Accept member
+ alert title
+
Accepted conditions
Accepted conditions
@@ -1582,11 +1597,26 @@ set passcode view
Chat will be deleted for you - this cannot be undone!
No comment provided by engineer.
+
+ Chat with admins
+ Chat with admins
+ chat toolbar
+
+
+ Chat with member
+ Chat with member
+ No comment provided by engineer.
+
Chats
Chats
No comment provided by engineer.
+
+ Chats with members
+ Chats with members
+ No comment provided by engineer.
+
Check messages every 20 min.
Check messages every 20 min.
@@ -2402,6 +2432,11 @@ swipe action
Delete chat profile?
No comment provided by engineer.
+
+ Delete chat with member?
+ Delete chat with member?
+ alert title
+
Delete chat?
Delete chat?
@@ -2825,7 +2860,7 @@ swipe action
Don't show again
Don't show again
- No comment provided by engineer.
+ alert action
Done
@@ -3143,6 +3178,11 @@ chat item action
Error accepting contact request
No comment provided by engineer.
+
+ Error accepting member
+ Error accepting member
+ alert title
+
Error adding member(s)
Error adding member(s)
@@ -3238,6 +3278,11 @@ chat item action
Error deleting chat database
No comment provided by engineer.
+
+ Error deleting chat with member
+ Error deleting chat with member
+ alert title
+
Error deleting chat!
Error deleting chat!
@@ -3346,7 +3391,7 @@ chat item action
Error removing member
Error removing member
- No comment provided by engineer.
+ alert title
Error reordering lists
@@ -4730,6 +4775,11 @@ This is your link for group %@!
Member
No comment provided by engineer.
+
+ Member admission
+ Member admission
+ No comment provided by engineer.
+
Member inactive
Member inactive
@@ -4765,6 +4815,11 @@ This is your link for group %@!
Member will be removed from group - this cannot be undone!
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ Member will join the group, accept member?
+ alert message
+
Members can add message reactions.
Members can add message reactions.
@@ -5185,6 +5240,11 @@ This is your link for group %@!
New member role
No comment provided by engineer.
+
+ New member wants to join the group.
+ New member wants to join the group.
+ rcv group event chat item
+
New message
New message
@@ -5225,6 +5285,11 @@ This is your link for group %@!
No chats in list %@
No comment provided by engineer.
+
+ No chats with members
+ No chats with members
+ No comment provided by engineer.
+
No contacts selected
No contacts selected
@@ -5417,7 +5482,8 @@ This is your link for group %@!
Ok
Ok
- alert button
+ alert action
+alert button
Old database
@@ -5829,6 +5895,11 @@ Error: %@
Please try to disable and re-enable notfications.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ Please wait for group moderators to review your request to join the group.
+ snd group event chat item
+
Please wait for token activation to complete.
Please wait for token activation to complete.
@@ -6282,6 +6353,11 @@ swipe action
Reject contact request
No comment provided by engineer.
+
+ Reject member?
+ Reject member?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
Relay server is only used if necessary. Another party can observe your IP address.
@@ -6392,6 +6468,11 @@ swipe action
Report reason?
No comment provided by engineer.
+
+ Report sent to moderators
+ Report sent to moderators
+ alert title
+
Report spam: only group moderators will see it.
Report spam: only group moderators will see it.
@@ -6507,6 +6588,16 @@ swipe action
Review conditions
No comment provided by engineer.
+
+ Review members
+ Review members
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ Review members before admitting ("knocking").
+ admission stage description
+
Revoke
Revoke
@@ -6563,6 +6654,11 @@ chat item action
Save (and notify contacts)
alert button
+
+ Save admission settings?
+ Save admission settings?
+ alert title
+
Save and notify contact
Save and notify contact
@@ -7078,6 +7174,11 @@ chat item action
Set it instead of system authentication.
No comment provided by engineer.
+
+ Set member admission
+ Set member admission
+ No comment provided by engineer.
+
Set message expiration in chats.
Set message expiration in chats.
@@ -8840,6 +8941,11 @@ Repeat join request?
You can view invitation link again in connection details.
alert message
+
+ You can view your reports in Chat with admins.
+ You can view your reports in Chat with admins.
+ alert message
+
You can't send messages!
You can't send messages!
@@ -9142,6 +9248,11 @@ Repeat connection request?
above, then choose:
No comment provided by engineer.
+
+ accepted %@
+ accepted %@
+ rcv group event chat item
+
accepted call
accepted call
@@ -9152,6 +9263,11 @@ Repeat connection request?
accepted invitation
chat list item title
+
+ accepted you
+ accepted you
+ rcv group event chat item
+
admin
admin
@@ -9172,6 +9288,11 @@ Repeat connection request?
agreeing encryption…
chat item text
+
+ all
+ all
+ member criteria value
+
all members
all members
@@ -9258,6 +9379,11 @@ marked deleted chat item preview text
calling…
call status
+
+ can't send messages
+ can't send messages
+ No comment provided by engineer.
+
cancelled %@
cancelled %@
@@ -9363,6 +9489,16 @@ marked deleted chat item preview text
contact %1$@ changed to %2$@
profile update event chat item
+
+ contact deleted
+ contact deleted
+ No comment provided by engineer.
+
+
+ contact disabled
+ contact disabled
+ No comment provided by engineer.
+
contact has e2e encryption
contact has e2e encryption
@@ -9373,6 +9509,11 @@ marked deleted chat item preview text
contact has no e2e encryption
No comment provided by engineer.
+
+ contact not ready
+ contact not ready
+ No comment provided by engineer.
+
creator
creator
@@ -9544,6 +9685,11 @@ pref value
group deleted
No comment provided by engineer.
+
+ group is deleted
+ group is deleted
+ No comment provided by engineer.
+
group profile updated
group profile updated
@@ -9669,6 +9815,11 @@ pref value
connected
rcv group event chat item
+
+ member has old version
+ member has old version
+ No comment provided by engineer.
+
message
message
@@ -9734,6 +9885,11 @@ pref value
no text
copied message info in history
+
+ not synchronized
+ not synchronized
+ No comment provided by engineer.
+
observer
observer
@@ -9744,6 +9900,7 @@ pref value
off
enabled status
group pref value
+member criteria value
time to disappear
@@ -9796,6 +9953,11 @@ time to disappear
pending approval
No comment provided by engineer.
+
+ pending review
+ pending review
+ No comment provided by engineer.
+
quantum resistant e2e encryption
quantum resistant e2e encryption
@@ -9836,6 +9998,11 @@ time to disappear
removed contact address
profile update event chat item
+
+ removed from group
+ removed from group
+ No comment provided by engineer.
+
removed profile picture
removed profile picture
@@ -9846,11 +10013,26 @@ time to disappear
removed you
rcv group event chat item
+
+ request to join rejected
+ request to join rejected
+ No comment provided by engineer.
+
requested to connect
requested to connect
chat list item title
+
+ review
+ review
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ reviewed by admins
+ No comment provided by engineer.
+
saved
saved
@@ -10040,6 +10222,11 @@ last received msg: %2$@
you
No comment provided by engineer.
+
+ you accepted this member
+ you accepted this member
+ snd group event chat item
+
you are invited to group
you are invited to group
diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff
index d39fb61249..3c3ae9ff46 100644
--- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff
+++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff
@@ -565,6 +565,14 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ alert action
+
+
+ Accept as observer
+ alert action
+
Accept conditions
Aceptar condiciones
@@ -586,6 +594,10 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ alert title
+
Accepted conditions
Condiciones aceptadas
@@ -1582,11 +1594,23 @@ set passcode view
El chat será eliminado para tí. ¡No puede deshacerse!
No comment provided by engineer.
+
+ Chat with admins
+ chat toolbar
+
+
+ Chat with member
+ No comment provided by engineer.
+
Chats
Chats
No comment provided by engineer.
+
+ Chats with members
+ No comment provided by engineer.
+
Check messages every 20 min.
Comprobar mensajes cada 20 min.
@@ -2402,6 +2426,10 @@ swipe action
¿Eliminar perfil?
No comment provided by engineer.
+
+ Delete chat with member?
+ alert title
+
Delete chat?
¿Eliminar chat?
@@ -2825,7 +2853,7 @@ swipe action
Don't show again
No volver a mostrar
- No comment provided by engineer.
+ alert action
Done
@@ -3143,6 +3171,10 @@ chat item action
Error al aceptar solicitud del contacto
No comment provided by engineer.
+
+ Error accepting member
+ alert title
+
Error adding member(s)
Error al añadir miembro(s)
@@ -3238,6 +3270,10 @@ chat item action
Error al eliminar base de datos
No comment provided by engineer.
+
+ Error deleting chat with member
+ alert title
+
Error deleting chat!
¡Error al eliminar chat!
@@ -3346,7 +3382,7 @@ chat item action
Error removing member
Error al expulsar miembro
- No comment provided by engineer.
+ alert title
Error reordering lists
@@ -4730,6 +4766,10 @@ This is your link for group %@!
Miembro
No comment provided by engineer.
+
+ Member admission
+ No comment provided by engineer.
+
Member inactive
Miembro inactivo
@@ -4765,6 +4805,10 @@ This is your link for group %@!
El miembro será expulsado del grupo. ¡No puede deshacerse!
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ alert message
+
Members can add message reactions.
Los miembros pueden añadir reacciones a los mensajes.
@@ -5185,6 +5229,10 @@ This is your link for group %@!
Nuevo rol de miembro
No comment provided by engineer.
+
+ New member wants to join the group.
+ rcv group event chat item
+
New message
Mensaje nuevo
@@ -5225,6 +5273,10 @@ This is your link for group %@!
Sin chats en la lista %@
No comment provided by engineer.
+
+ No chats with members
+ No comment provided by engineer.
+
No contacts selected
Ningún contacto seleccionado
@@ -5417,7 +5469,8 @@ This is your link for group %@!
Ok
Ok
- alert button
+ alert action
+alert button
Old database
@@ -5828,6 +5881,10 @@ Error: %@
Por favor, intenta desactivar y reactivar las notificaciones.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ snd group event chat item
+
Please wait for token activation to complete.
Por favor, espera a que el token de activación se complete.
@@ -6281,6 +6338,10 @@ swipe action
Rechazar solicitud de contacto
No comment provided by engineer.
+
+ Reject member?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
El servidor de retransmisión sólo se usa en caso de necesidad. Un tercero podría ver tu IP.
@@ -6391,6 +6452,10 @@ swipe action
¿Motivo del informe?
No comment provided by engineer.
+
+ Report sent to moderators
+ alert title
+
Report spam: only group moderators will see it.
Informar de spam: sólo los moderadores del grupo lo verán.
@@ -6506,6 +6571,14 @@ swipe action
Revisar condiciones
No comment provided by engineer.
+
+ Review members
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ admission stage description
+
Revoke
Revocar
@@ -6562,6 +6635,10 @@ chat item action
Guardar (y notificar contactos)
alert button
+
+ Save admission settings?
+ alert title
+
Save and notify contact
Guardar y notificar contacto
@@ -7077,6 +7154,10 @@ chat item action
Úsalo en lugar de la autenticación del sistema.
No comment provided by engineer.
+
+ Set member admission
+ No comment provided by engineer.
+
Set message expiration in chats.
Establece el vencimiento para los mensajes en los chats.
@@ -8839,6 +8920,10 @@ Repeat join request?
Podrás ver el enlace de invitación en detalles de conexión.
alert message
+
+ You can view your reports in Chat with admins.
+ alert message
+
You can't send messages!
¡No puedes enviar mensajes!
@@ -9141,6 +9226,10 @@ Repeat connection request?
y después elige:
No comment provided by engineer.
+
+ accepted %@
+ rcv group event chat item
+
accepted call
llamada aceptada
@@ -9151,6 +9240,10 @@ Repeat connection request?
invitación aceptada
chat list item title
+
+ accepted you
+ rcv group event chat item
+
admin
administrador
@@ -9171,6 +9264,10 @@ Repeat connection request?
acordando cifrado…
chat item text
+
+ all
+ member criteria value
+
all members
todos los miembros
@@ -9257,6 +9354,10 @@ marked deleted chat item preview text
llamando…
call status
+
+ can't send messages
+ No comment provided by engineer.
+
cancelled %@
cancelado %@
@@ -9362,6 +9463,14 @@ marked deleted chat item preview text
el contacto %1$@ ha cambiado a %2$@
profile update event chat item
+
+ contact deleted
+ No comment provided by engineer.
+
+
+ contact disabled
+ No comment provided by engineer.
+
contact has e2e encryption
el contacto dispone de cifrado de extremo a extremo
@@ -9372,6 +9481,10 @@ marked deleted chat item preview text
el contacto no dispone de cifrado de extremo a extremo
No comment provided by engineer.
+
+ contact not ready
+ No comment provided by engineer.
+
creator
creador
@@ -9543,6 +9656,10 @@ pref value
grupo eliminado
No comment provided by engineer.
+
+ group is deleted
+ No comment provided by engineer.
+
group profile updated
perfil de grupo actualizado
@@ -9668,6 +9785,10 @@ pref value
conectado
rcv group event chat item
+
+ member has old version
+ No comment provided by engineer.
+
message
mensaje
@@ -9733,6 +9854,10 @@ pref value
sin texto
copied message info in history
+
+ not synchronized
+ No comment provided by engineer.
+
observer
observador
@@ -9743,6 +9868,7 @@ pref value
desactivado
enabled status
group pref value
+member criteria value
time to disappear
@@ -9795,6 +9921,10 @@ time to disappear
pendiente de aprobación
No comment provided by engineer.
+
+ pending review
+ No comment provided by engineer.
+
quantum resistant e2e encryption
cifrado e2e resistente a tecnología cuántica
@@ -9835,6 +9965,10 @@ time to disappear
dirección de contacto eliminada
profile update event chat item
+
+ removed from group
+ No comment provided by engineer.
+
removed profile picture
ha eliminado la imagen del perfil
@@ -9845,11 +9979,23 @@ time to disappear
te ha expulsado
rcv group event chat item
+
+ request to join rejected
+ No comment provided by engineer.
+
requested to connect
solicitado para conectar
chat list item title
+
+ review
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ No comment provided by engineer.
+
saved
guardado
@@ -10039,6 +10185,10 @@ last received msg: %2$@
tu
No comment provided by engineer.
+
+ you accepted this member
+ snd group event chat item
+
you are invited to group
has sido invitado a un grupo
diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff
index a54666bb10..7c93c5b0bb 100644
--- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff
+++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff
@@ -527,6 +527,14 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ alert action
+
+
+ Accept as observer
+ alert action
+
Accept conditions
No comment provided by engineer.
@@ -547,6 +555,10 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ alert title
+
Accepted conditions
No comment provided by engineer.
@@ -1437,11 +1449,23 @@ set passcode view
Chat will be deleted for you - this cannot be undone!
No comment provided by engineer.
+
+ Chat with admins
+ chat toolbar
+
+
+ Chat with member
+ No comment provided by engineer.
+
Chats
Keskustelut
No comment provided by engineer.
+
+ Chats with members
+ No comment provided by engineer.
+
Check messages every 20 min.
No comment provided by engineer.
@@ -2174,6 +2198,10 @@ swipe action
Poista keskusteluprofiili?
No comment provided by engineer.
+
+ Delete chat with member?
+ alert title
+
Delete chat?
No comment provided by engineer.
@@ -2565,7 +2593,7 @@ swipe action
Don't show again
Älä näytä uudelleen
- No comment provided by engineer.
+ alert action
Done
@@ -2858,6 +2886,10 @@ chat item action
Virhe kontaktipyynnön hyväksymisessä
No comment provided by engineer.
+
+ Error accepting member
+ alert title
+
Error adding member(s)
Virhe lisättäessä jäseniä
@@ -2944,6 +2976,10 @@ chat item action
Virhe keskustelujen tietokannan poistamisessa
No comment provided by engineer.
+
+ Error deleting chat with member
+ alert title
+
Error deleting chat!
Virhe keskutelun poistamisessa!
@@ -3044,7 +3080,7 @@ chat item action
Error removing member
Virhe poistettaessa jäsentä
- No comment provided by engineer.
+ alert title
Error reordering lists
@@ -4311,6 +4347,10 @@ This is your link for group %@!
Jäsen
No comment provided by engineer.
+
+ Member admission
+ No comment provided by engineer.
+
Member inactive
item status text
@@ -4342,6 +4382,10 @@ This is your link for group %@!
Jäsen poistetaan ryhmästä - tätä ei voi perua!
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ alert message
+
Members can add message reactions.
Ryhmän jäsenet voivat lisätä viestireaktioita.
@@ -4716,6 +4760,10 @@ This is your link for group %@!
Uusi jäsenrooli
No comment provided by engineer.
+
+ New member wants to join the group.
+ rcv group event chat item
+
New message
Uusi viesti
@@ -4752,6 +4800,10 @@ This is your link for group %@!
No chats in list %@
No comment provided by engineer.
+
+ No chats with members
+ No comment provided by engineer.
+
No contacts selected
Kontakteja ei ole valittu
@@ -4922,7 +4974,8 @@ This is your link for group %@!
Ok
Ok
- alert button
+ alert action
+alert button
Old database
@@ -5296,6 +5349,10 @@ Error: %@
Please try to disable and re-enable notfications.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ snd group event chat item
+
Please wait for token activation to complete.
token info
@@ -5707,6 +5764,10 @@ swipe action
Hylkää yhteyspyyntö
No comment provided by engineer.
+
+ Reject member?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
Välityspalvelinta käytetään vain tarvittaessa. Toinen osapuoli voi tarkkailla IP-osoitettasi.
@@ -5805,6 +5866,10 @@ swipe action
Report reason?
No comment provided by engineer.
+
+ Report sent to moderators
+ alert title
+
Report spam: only group moderators will see it.
report reason
@@ -5908,6 +5973,14 @@ swipe action
Review conditions
No comment provided by engineer.
+
+ Review members
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ admission stage description
+
Revoke
Peruuta
@@ -5960,6 +6033,10 @@ chat item action
Tallenna (ja ilmoita kontakteille)
alert button
+
+ Save admission settings?
+ alert title
+
Save and notify contact
Tallenna ja ilmoita kontaktille
@@ -6430,6 +6507,10 @@ chat item action
Aseta se järjestelmän todennuksen sijaan.
No comment provided by engineer.
+
+ Set member admission
+ No comment provided by engineer.
+
Set message expiration in chats.
No comment provided by engineer.
@@ -8017,6 +8098,10 @@ Repeat join request?
You can view invitation link again in connection details.
alert message
+
+ You can view your reports in Chat with admins.
+ alert message
+
You can't send messages!
Et voi lähettää viestejä!
@@ -8303,6 +8388,10 @@ Repeat connection request?
edellä, valitse sitten:
No comment provided by engineer.
+
+ accepted %@
+ rcv group event chat item
+
accepted call
hyväksytty puhelu
@@ -8312,6 +8401,10 @@ Repeat connection request?
accepted invitation
chat list item title
+
+ accepted you
+ rcv group event chat item
+
admin
ylläpitäjä
@@ -8331,6 +8424,10 @@ Repeat connection request?
hyväksyy salausta…
chat item text
+
+ all
+ member criteria value
+
all members
feature role
@@ -8408,6 +8505,10 @@ marked deleted chat item preview text
soittaa…
call status
+
+ can't send messages
+ No comment provided by engineer.
+
cancelled %@
peruutettu %@
@@ -8511,6 +8612,14 @@ marked deleted chat item preview text
contact %1$@ changed to %2$@
profile update event chat item
+
+ contact deleted
+ No comment provided by engineer.
+
+
+ contact disabled
+ No comment provided by engineer.
+
contact has e2e encryption
kontaktilla on e2e-salaus
@@ -8521,6 +8630,10 @@ marked deleted chat item preview text
kontaktilla ei ole e2e-salausta
No comment provided by engineer.
+
+ contact not ready
+ No comment provided by engineer.
+
creator
luoja
@@ -8687,6 +8800,10 @@ pref value
ryhmä poistettu
No comment provided by engineer.
+
+ group is deleted
+ No comment provided by engineer.
+
group profile updated
ryhmäprofiili päivitetty
@@ -8809,6 +8926,10 @@ pref value
yhdistetty
rcv group event chat item
+
+ member has old version
+ No comment provided by engineer.
+
message
No comment provided by engineer.
@@ -8872,6 +8993,10 @@ pref value
ei tekstiä
copied message info in history
+
+ not synchronized
+ No comment provided by engineer.
+
observer
tarkkailija
@@ -8882,6 +9007,7 @@ pref value
pois
enabled status
group pref value
+member criteria value
time to disappear
@@ -8929,6 +9055,10 @@ time to disappear
pending approval
No comment provided by engineer.
+
+ pending review
+ No comment provided by engineer.
+
quantum resistant e2e encryption
chat item text
@@ -8966,6 +9096,10 @@ time to disappear
removed contact address
profile update event chat item
+
+ removed from group
+ No comment provided by engineer.
+
removed profile picture
profile update event chat item
@@ -8975,10 +9109,22 @@ time to disappear
poisti sinut
rcv group event chat item
+
+ request to join rejected
+ No comment provided by engineer.
+
requested to connect
chat list item title
+
+ review
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ No comment provided by engineer.
+
saved
No comment provided by engineer.
@@ -9149,6 +9295,10 @@ last received msg: %2$@
you
No comment provided by engineer.
+
+ you accepted this member
+ snd group event chat item
+
you are invited to group
sinut on kutsuttu ryhmään
diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff
index 59bde0650e..80b3428cfe 100644
--- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff
+++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff
@@ -565,6 +565,14 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ alert action
+
+
+ Accept as observer
+ alert action
+
Accept conditions
Accepter les conditions
@@ -586,6 +594,10 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ alert title
+
Accepted conditions
Conditions acceptées
@@ -1580,11 +1592,23 @@ set passcode view
Le discussion sera supprimé pour vous - il n'est pas possible de revenir en arrière !
No comment provided by engineer.
+
+ Chat with admins
+ chat toolbar
+
+
+ Chat with member
+ No comment provided by engineer.
+
Chats
Discussions
No comment provided by engineer.
+
+ Chats with members
+ No comment provided by engineer.
+
Check messages every 20 min.
Consulter les messages toutes les 20 minutes.
@@ -2400,6 +2424,10 @@ swipe action
Supprimer le profil du chat ?
No comment provided by engineer.
+
+ Delete chat with member?
+ alert title
+
Delete chat?
Supprimer la discussion ?
@@ -2823,7 +2851,7 @@ swipe action
Don't show again
Ne plus afficher
- No comment provided by engineer.
+ alert action
Done
@@ -3141,6 +3169,10 @@ chat item action
Erreur de validation de la demande de contact
No comment provided by engineer.
+
+ Error accepting member
+ alert title
+
Error adding member(s)
Erreur lors de l'ajout de membre·s
@@ -3236,6 +3268,10 @@ chat item action
Erreur lors de la suppression de la base de données du chat
No comment provided by engineer.
+
+ Error deleting chat with member
+ alert title
+
Error deleting chat!
Erreur lors de la suppression du chat !
@@ -3344,7 +3380,7 @@ chat item action
Error removing member
Erreur lors de la suppression d'un membre
- No comment provided by engineer.
+ alert title
Error reordering lists
@@ -4712,6 +4748,10 @@ Voici votre lien pour le groupe %@ !
Membre
No comment provided by engineer.
+
+ Member admission
+ No comment provided by engineer.
+
Member inactive
Membre inactif
@@ -4746,6 +4786,10 @@ Voici votre lien pour le groupe %@ !
Ce membre sera retiré du groupe - impossible de revenir en arrière !
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ alert message
+
Members can add message reactions.
Les membres du groupe peuvent ajouter des réactions aux messages.
@@ -5160,6 +5204,10 @@ Voici votre lien pour le groupe %@ !
Nouveau rôle
No comment provided by engineer.
+
+ New member wants to join the group.
+ rcv group event chat item
+
New message
Nouveau message
@@ -5197,6 +5245,10 @@ Voici votre lien pour le groupe %@ !
No chats in list %@
No comment provided by engineer.
+
+ No chats with members
+ No comment provided by engineer.
+
No contacts selected
Aucun contact sélectionné
@@ -5383,7 +5435,8 @@ Voici votre lien pour le groupe %@ !
Ok
Ok
- alert button
+ alert action
+alert button
Old database
@@ -5790,6 +5843,10 @@ Erreur : %@
Please try to disable and re-enable notfications.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ snd group event chat item
+
Please wait for token activation to complete.
token info
@@ -6234,6 +6291,10 @@ swipe action
Rejeter la demande de contact
No comment provided by engineer.
+
+ Reject member?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
Le serveur relais n'est utilisé que si nécessaire. Un tiers peut observer votre adresse IP.
@@ -6339,6 +6400,10 @@ swipe action
Report reason?
No comment provided by engineer.
+
+ Report sent to moderators
+ alert title
+
Report spam: only group moderators will see it.
report reason
@@ -6449,6 +6514,14 @@ swipe action
Vérifier les conditions
No comment provided by engineer.
+
+ Review members
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ admission stage description
+
Revoke
Révoquer
@@ -6505,6 +6578,10 @@ chat item action
Enregistrer (et en informer les contacts)
alert button
+
+ Save admission settings?
+ alert title
+
Save and notify contact
Enregistrer et en informer le contact
@@ -7017,6 +7094,10 @@ chat item action
Il permet de remplacer l'authentification du système.
No comment provided by engineer.
+
+ Set member admission
+ No comment provided by engineer.
+
Set message expiration in chats.
No comment provided by engineer.
@@ -8762,6 +8843,10 @@ Répéter la demande d'adhésion ?
Vous pouvez à nouveau consulter le lien d'invitation dans les détails de la connexion.
alert message
+
+ You can view your reports in Chat with admins.
+ alert message
+
You can't send messages!
Vous ne pouvez pas envoyer de messages !
@@ -9063,6 +9148,10 @@ Répéter la demande de connexion ?
ci-dessus, puis choisissez :
No comment provided by engineer.
+
+ accepted %@
+ rcv group event chat item
+
accepted call
appel accepté
@@ -9073,6 +9162,10 @@ Répéter la demande de connexion ?
invitation acceptée
chat list item title
+
+ accepted you
+ rcv group event chat item
+
admin
admin
@@ -9093,6 +9186,10 @@ Répéter la demande de connexion ?
négociation du chiffrement…
chat item text
+
+ all
+ member criteria value
+
all members
tous les membres
@@ -9178,6 +9275,10 @@ marked deleted chat item preview text
appel…
call status
+
+ can't send messages
+ No comment provided by engineer.
+
cancelled %@
annulé %@
@@ -9283,6 +9384,14 @@ marked deleted chat item preview text
le contact %1$@ est devenu %2$@
profile update event chat item
+
+ contact deleted
+ No comment provided by engineer.
+
+
+ contact disabled
+ No comment provided by engineer.
+
contact has e2e encryption
Ce contact a le chiffrement de bout en bout
@@ -9293,6 +9402,10 @@ marked deleted chat item preview text
Ce contact n'a pas le chiffrement de bout en bout
No comment provided by engineer.
+
+ contact not ready
+ No comment provided by engineer.
+
creator
créateur
@@ -9464,6 +9577,10 @@ pref value
groupe supprimé
No comment provided by engineer.
+
+ group is deleted
+ No comment provided by engineer.
+
group profile updated
mise à jour du profil de groupe
@@ -9589,6 +9706,10 @@ pref value
est connecté·e
rcv group event chat item
+
+ member has old version
+ No comment provided by engineer.
+
message
message
@@ -9653,6 +9774,10 @@ pref value
aucun texte
copied message info in history
+
+ not synchronized
+ No comment provided by engineer.
+
observer
observateur
@@ -9663,6 +9788,7 @@ pref value
off
enabled status
group pref value
+member criteria value
time to disappear
@@ -9713,6 +9839,10 @@ time to disappear
pending approval
No comment provided by engineer.
+
+ pending review
+ No comment provided by engineer.
+
quantum resistant e2e encryption
chiffrement e2e résistant post-quantique
@@ -9752,6 +9882,10 @@ time to disappear
suppression de l'adresse de contact
profile update event chat item
+
+ removed from group
+ No comment provided by engineer.
+
removed profile picture
suppression de la photo de profil
@@ -9762,11 +9896,23 @@ time to disappear
vous a retiré
rcv group event chat item
+
+ request to join rejected
+ No comment provided by engineer.
+
requested to connect
demande à se connecter
chat list item title
+
+ review
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ No comment provided by engineer.
+
saved
enregistré
@@ -9956,6 +10102,10 @@ dernier message reçu : %2$@
vous
No comment provided by engineer.
+
+ you accepted this member
+ snd group event chat item
+
you are invited to group
vous êtes invité·e au groupe
diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff
index 78bee138e4..5fd4c21027 100644
--- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff
+++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff
@@ -565,6 +565,16 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ Befogadás tagként
+ alert action
+
+
+ Accept as observer
+ Befogadás megfigyelőként
+ alert action
+
Accept conditions
Feltételek elfogadása
@@ -586,6 +596,11 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ Tag befogadása
+ alert title
+
Accepted conditions
Elfogadott feltételek
@@ -1582,11 +1597,26 @@ set passcode view
A csevegés törölve lesz az Ön számára – ez a művelet nem vonható vissza!
No comment provided by engineer.
+
+ Chat with admins
+ Csevegés az adminisztrátorokkal
+ chat toolbar
+
+
+ Chat with member
+ Csevegés a taggal
+ No comment provided by engineer.
+
Chats
Csevegések
No comment provided by engineer.
+
+ Chats with members
+ Csevegés a tagokkal
+ No comment provided by engineer.
+
Check messages every 20 min.
Üzenetek ellenőrzése 20 percenként.
@@ -2240,7 +2270,7 @@ Ez a saját egyszer használható meghívója!
Database IDs and Transport isolation option.
- Adatbázis-azonosítók és átvitel-izolációs beállítások.
+ Adatbázis-azonosítók és átvitelelkülönítési beállítások.
No comment provided by engineer.
@@ -2338,7 +2368,7 @@ Ez a saját egyszer használható meghívója!
Decryption error
- Titkosítás visszafejtési hiba
+ Titkosításvisszafejtési hiba
message decrypt error item
@@ -2402,6 +2432,11 @@ swipe action
Törli a csevegési profilt?
No comment provided by engineer.
+
+ Delete chat with member?
+ Törli a taggal való csevegést?
+ alert title
+
Delete chat?
Törli a csevegést?
@@ -2674,7 +2709,7 @@ swipe action
Different names, avatars and transport isolation.
- Különböző nevek, profilképek és átvitel-izoláció.
+ Különböző nevek, profilképek és átvitelizoláció.
No comment provided by engineer.
@@ -2825,7 +2860,7 @@ swipe action
Don't show again
Ne mutasd újra
- No comment provided by engineer.
+ alert action
Done
@@ -3143,6 +3178,11 @@ chat item action
Hiba történt a meghívási kérés elfogadásakor
No comment provided by engineer.
+
+ Error accepting member
+ Hiba a tag befogadásakor
+ alert title
+
Error adding member(s)
Hiba történt a tag(ok) hozzáadásakor
@@ -3238,6 +3278,11 @@ chat item action
Hiba történt a csevegési adatbázis törlésekor
No comment provided by engineer.
+
+ Error deleting chat with member
+ Hiba a taggal való csevegés törlésekor
+ alert title
+
Error deleting chat!
Hiba történt a csevegés törlésekor!
@@ -3265,7 +3310,7 @@ chat item action
Error deleting user profile
- Hiba történt a felhasználó-profil törlésekor
+ Hiba történt a felhasználói profil törlésekor
No comment provided by engineer.
@@ -3346,7 +3391,7 @@ chat item action
Error removing member
Hiba történt a tag eltávolításakor
- No comment provided by engineer.
+ alert title
Error reordering lists
@@ -4449,7 +4494,7 @@ További fejlesztések hamarosan!
It can happen when you or your connection used the old database backup.
- Ez akkor fordulhat elő, ha Ön vagy a partnere régi adatbázis biztonsági mentést használt.
+ Ez akkor fordulhat elő, ha Ön vagy a partnere egy régi adatbázis biztonsági mentését használta.
No comment provided by engineer.
@@ -4459,7 +4504,7 @@ További fejlesztések hamarosan!
3. The connection was compromised.
Ez akkor fordulhat elő, ha:
1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.
-2. Nem sikerült az üzenetet visszafejteni, mert Ön, vagy a partnere régebbi adatbázis biztonsági mentést használt.
+2. Nem sikerült az üzenetet visszafejteni, mert Ön, vagy a partnere egy régi adatbázis biztonsági mentését használta.
3. A kapcsolat sérült.
No comment provided by engineer.
@@ -4730,6 +4775,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz!
Tag
No comment provided by engineer.
+
+ Member admission
+ Tagbefogadás
+ No comment provided by engineer.
+
Member inactive
Inaktív tag
@@ -4765,6 +4815,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz!
A tag el lesz távolítva a csoportból – ez a művelet nem vonható vissza!
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ A tag csatlakozni akar a csoporthoz, befogadja a tagot?
+ alert message
+
Members can add message reactions.
A tagok reakciókat adhatnak hozzá az üzenetekhez.
@@ -4942,12 +4997,12 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz!
Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.
- Az üzenetek, a fájlok és a hívások **végpontok közötti titkosítással**, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve.
+ Az üzenetek, a fájlok és a hívások **végpontok közötti titkosítással**, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve.
No comment provided by engineer.
Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.
- Az üzenetek, a fájlok és a hívások **végpontok közötti kvantumbiztos titkosítással**, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve.
+ Az üzenetek, a fájlok és a hívások **végpontok közötti kvantumbiztos titkosítással**, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve.
No comment provided by engineer.
@@ -5185,6 +5240,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz!
Új tag szerepköre
No comment provided by engineer.
+
+ New member wants to join the group.
+ Új tag szeretne csatlakozni a csoporthoz.
+ rcv group event chat item
+
New message
Új üzenet
@@ -5225,6 +5285,11 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz!
Nincsenek csevegések a(z) %@ nevű listában
No comment provided by engineer.
+
+ No chats with members
+ Nincsenek csevegések a tagokkal
+ No comment provided by engineer.
+
No contacts selected
Nincs partner kijelölve
@@ -5347,7 +5412,7 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz!
No user identifiers.
- Nincsenek felhasználó-azonosítók.
+ Nincsenek felhasználói azonosítók.
No comment provided by engineer.
@@ -5417,7 +5482,8 @@ Ez a saját hivatkozása a(z) %@ nevű csoporthoz!
Ok
Rendben
- alert button
+ alert action
+alert button
Old database
@@ -5575,6 +5641,7 @@ VPN engedélyezése szükséges.
Open link?
+ Megnyitja a hivatkozást?
alert title
@@ -5828,6 +5895,11 @@ Hiba: %@
Próbálja meg letiltani és újra engedélyezni az értesítéseket.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ Várja meg, amíg a csoport moderátorai áttekintik a csoporthoz való csatlakozási kérelmét.
+ snd group event chat item
+
Please wait for token activation to complete.
Várjon, amíg a token aktiválása befejeződik.
@@ -6281,14 +6353,19 @@ swipe action
Meghívási kérés elutasítása
No comment provided by engineer.
+
+ Reject member?
+ Elutasítja a tagot?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
- A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címet.
+ A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét.
No comment provided by engineer.
Relay server protects your IP address, but it can observe the duration of the call.
- A továbbítókiszolgáló megvédi az Ön IP-címét, de megfigyelheti a hívás időtartamát.
+ A továbbítókiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát.
No comment provided by engineer.
@@ -6391,6 +6468,11 @@ swipe action
Jelentés indoklása?
No comment provided by engineer.
+
+ Report sent to moderators
+ A jelentés el lett küldve a moderátoroknak
+ alert title
+
Report spam: only group moderators will see it.
Kéretlen tartalom jelentése: csak a csoport moderátorai látják.
@@ -6506,6 +6588,16 @@ swipe action
Feltételek felülvizsgálata
No comment provided by engineer.
+
+ Review members
+ Tagok áttekintése
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ Tagok áttekintése a befogadás előtt (kopogtatás).
+ admission stage description
+
Revoke
Visszavonás
@@ -6562,6 +6654,11 @@ chat item action
Mentés (és a partnerek értesítése)
alert button
+
+ Save admission settings?
+ Elmenti a befogadási beállításokat?
+ alert title
+
Save and notify contact
Mentés és a partner értesítése
@@ -7077,6 +7174,11 @@ chat item action
Beállítás a rendszer-hitelesítés helyett.
No comment provided by engineer.
+
+ Set member admission
+ Tagbefogadás beállítása
+ No comment provided by engineer.
+
Set message expiration in chats.
Üzenetek eltűnési idejének módosítása a csevegésekben.
@@ -7335,7 +7437,7 @@ chat item action
SimpleX protocols reviewed by Trail of Bits.
- A SimpleX Chat biztonsága a Trail of Bits által lett felülvizsgálva.
+ A SimpleX-protokollokat a Trail of Bits auditálta.
No comment provided by engineer.
@@ -8027,7 +8129,7 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll
Transport isolation
- Átvitel-izoláció
+ Átvitelelkülönítés
No comment provided by engineer.
@@ -8339,7 +8441,7 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso
Use private routing with unknown servers when IP address is not protected.
- Használjon privát útválasztást ismeretlen kiszolgálókkal, ha az IP-cím nem védett.
+ Használjon privát útválasztást az ismeretlen kiszolgálókkal, ha az IP-cím nem védett.
No comment provided by engineer.
@@ -8624,12 +8726,12 @@ A kapcsolódáshoz kérje meg a partnerét, hogy hozzon létre egy másik kapcso
Without Tor or VPN, your IP address will be visible to file servers.
- Tor vagy VPN nélkül az Ön IP-címe látható lesz a fájlkiszolgálók számára.
+ Tor vagy VPN nélkül az IP-címe láthatóvá válik a fájlkiszolgálók számára.
No comment provided by engineer.
Without Tor or VPN, your IP address will be visible to these XFTP relays: %@.
- Tor vagy VPN nélkül az Ön IP-címe látható lesz a következő XFTP-továbbítókiszolgálók számára: %@.
+ Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-továbbítókiszolgálók számára: %@.
alert message
@@ -8839,6 +8941,11 @@ Megismétli a meghívási kérést?
A meghívási hivatkozást újra megtekintheti a kapcsolat részleteinél.
alert message
+
+ You can view your reports in Chat with admins.
+ A jelentéseket megtekintheti a „Csevegés az adminisztrátorokkal” menüben.
+ alert message
+
You can't send messages!
Nem lehet üzeneteket küldeni!
@@ -9078,7 +9185,7 @@ Megismétli a meghívási kérést?
Your profile is stored on your device and only shared with your contacts.
- A profilja csak a partnereivel van megosztva.
+ A profilja az eszközén van tárolva és csak a partnereivel van megosztva.
No comment provided by engineer.
@@ -9141,6 +9248,11 @@ Megismétli a meghívási kérést?
gombra fent, majd válassza ki:
No comment provided by engineer.
+
+ accepted %@
+ befogadta őt: %@
+ rcv group event chat item
+
accepted call
fogadott hívás
@@ -9151,6 +9263,11 @@ Megismétli a meghívási kérést?
elfogadott meghívó
chat list item title
+
+ accepted you
+ befogadta Önt
+ rcv group event chat item
+
admin
adminisztrátor
@@ -9171,6 +9288,11 @@ Megismétli a meghívási kérést?
titkosítás elfogadása…
chat item text
+
+ all
+ összes
+ member criteria value
+
all members
összes tag
@@ -9257,6 +9379,11 @@ marked deleted chat item preview text
hívás…
call status
+
+ can't send messages
+ nem lehet üzeneteket küldeni
+ No comment provided by engineer.
+
cancelled %@
%@ visszavonva
@@ -9362,6 +9489,16 @@ marked deleted chat item preview text
%1$@ a következőre módosította a nevét: %2$@
profile update event chat item
+
+ contact deleted
+ partner törölve
+ No comment provided by engineer.
+
+
+ contact disabled
+ partner letiltva
+ No comment provided by engineer.
+
contact has e2e encryption
a partner e2e titkosítással rendelkezik
@@ -9372,6 +9509,11 @@ marked deleted chat item preview text
a partner nem rendelkezik e2e titkosítással
No comment provided by engineer.
+
+ contact not ready
+ a kapcsolat nem áll készen
+ No comment provided by engineer.
+
creator
készítő
@@ -9543,6 +9685,11 @@ pref value
a csoport törölve
No comment provided by engineer.
+
+ group is deleted
+ csoport törölve
+ No comment provided by engineer.
+
group profile updated
csoportprofil frissítve
@@ -9668,6 +9815,11 @@ pref value
kapcsolódott
rcv group event chat item
+
+ member has old version
+ a tag régi verziót használ
+ No comment provided by engineer.
+
message
üzenet
@@ -9733,6 +9885,11 @@ pref value
nincs szöveg
copied message info in history
+
+ not synchronized
+ nincs szinkronizálva
+ No comment provided by engineer.
+
observer
megfigyelő
@@ -9743,6 +9900,7 @@ pref value
kikapcsolva
enabled status
group pref value
+member criteria value
time to disappear
@@ -9795,6 +9953,11 @@ time to disappear
jóváhagyásra vár
No comment provided by engineer.
+
+ pending review
+ függőben lévő áttekintés
+ No comment provided by engineer.
+
quantum resistant e2e encryption
végpontok közötti kvantumbiztos titkosítás
@@ -9835,6 +9998,11 @@ time to disappear
eltávolította a kapcsolattartási címet
profile update event chat item
+
+ removed from group
+ eltávolítva a csoportból
+ No comment provided by engineer.
+
removed profile picture
eltávolította a profilképét
@@ -9845,11 +10013,26 @@ time to disappear
eltávolította Önt
rcv group event chat item
+
+ request to join rejected
+ csatlakozási kérelem elutasítva
+ No comment provided by engineer.
+
requested to connect
Függőben lévő meghívási kérelem
chat list item title
+
+ review
+ áttekintés
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ áttekintve a moderátorok által
+ No comment provided by engineer.
+
saved
mentett
@@ -10039,6 +10222,11 @@ utoljára fogadott üzenet: %2$@
Ön
No comment provided by engineer.
+
+ you accepted this member
+ Ön befogadta ezt a tagot
+ snd group event chat item
+
you are invited to group
Ön meghívást kapott a csoportba
diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff
index cf5f61918f..d3c2a139cc 100644
--- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff
+++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff
@@ -565,6 +565,16 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ Accetta come membro
+ alert action
+
+
+ Accept as observer
+ Accetta come osservatore
+ alert action
+
Accept conditions
Accetta le condizioni
@@ -586,6 +596,11 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ Accetta membro
+ alert title
+
Accepted conditions
Condizioni accettate
@@ -1582,11 +1597,26 @@ set passcode view
La chat verrà eliminata solo per te, non è reversibile!
No comment provided by engineer.
+
+ Chat with admins
+ Chat con amministratori
+ chat toolbar
+
+
+ Chat with member
+ Chatta con il membro
+ No comment provided by engineer.
+
Chats
Chat
No comment provided by engineer.
+
+ Chats with members
+ Chat con membri
+ No comment provided by engineer.
+
Check messages every 20 min.
Controlla i messaggi ogni 20 min.
@@ -2402,6 +2432,11 @@ swipe action
Eliminare il profilo di chat?
No comment provided by engineer.
+
+ Delete chat with member?
+ Eliminare la chat con il membro?
+ alert title
+
Delete chat?
Eliminare la chat?
@@ -2825,7 +2860,7 @@ swipe action
Don't show again
Non mostrare più
- No comment provided by engineer.
+ alert action
Done
@@ -3143,6 +3178,11 @@ chat item action
Errore nell'accettazione della richiesta di contatto
No comment provided by engineer.
+
+ Error accepting member
+ Errore di accettazione del membro
+ alert title
+
Error adding member(s)
Errore di aggiunta membro/i
@@ -3238,6 +3278,11 @@ chat item action
Errore nell'eliminazione del database della chat
No comment provided by engineer.
+
+ Error deleting chat with member
+ Errore di eliminazione della chat con il membro
+ alert title
+
Error deleting chat!
Errore nell'eliminazione della chat!
@@ -3346,7 +3391,7 @@ chat item action
Error removing member
Errore nella rimozione del membro
- No comment provided by engineer.
+ alert title
Error reordering lists
@@ -4730,6 +4775,11 @@ Questo è il tuo link per il gruppo %@!
Membro
No comment provided by engineer.
+
+ Member admission
+ Ammissione del membro
+ No comment provided by engineer.
+
Member inactive
Membro inattivo
@@ -4765,6 +4815,11 @@ Questo è il tuo link per il gruppo %@!
Il membro verrà rimosso dal gruppo, non è reversibile!
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ Il membro entrerà nel gruppo, accettarlo?
+ alert message
+
Members can add message reactions.
I membri del gruppo possono aggiungere reazioni ai messaggi.
@@ -5185,6 +5240,11 @@ Questo è il tuo link per il gruppo %@!
Nuovo ruolo del membro
No comment provided by engineer.
+
+ New member wants to join the group.
+ Un nuovo membro vuole entrare nel gruppo.
+ rcv group event chat item
+
New message
Nuovo messaggio
@@ -5225,6 +5285,11 @@ Questo è il tuo link per il gruppo %@!
Nessuna chat nell'elenco %@
No comment provided by engineer.
+
+ No chats with members
+ Nessuna chat con membri
+ No comment provided by engineer.
+
No contacts selected
Nessun contatto selezionato
@@ -5417,7 +5482,8 @@ Questo è il tuo link per il gruppo %@!
Ok
Ok
- alert button
+ alert action
+alert button
Old database
@@ -5575,6 +5641,7 @@ Richiede l'attivazione della VPN.
Open link?
+ Aprire il link?
alert title
@@ -5828,6 +5895,11 @@ Errore: %@
Prova a disattivare e riattivare le notifiche.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ Attendi che i moderatori del gruppo revisionino la tua richiesta di entrare nel gruppo.
+ snd group event chat item
+
Please wait for token activation to complete.
Attendi il completamento dell'attivazione del token.
@@ -6281,6 +6353,11 @@ swipe action
Rifiuta la richiesta di contatto
No comment provided by engineer.
+
+ Reject member?
+ Rifiutare il membro?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
Il server relay viene usato solo se necessario. Un altro utente può osservare il tuo indirizzo IP.
@@ -6391,6 +6468,11 @@ swipe action
Motivo della segnalazione?
No comment provided by engineer.
+
+ Report sent to moderators
+ Segnalazione inviata ai moderatori
+ alert title
+
Report spam: only group moderators will see it.
Segnala spam: solo i moderatori del gruppo lo vedranno.
@@ -6506,6 +6588,16 @@ swipe action
Leggi le condizioni
No comment provided by engineer.
+
+ Review members
+ Revisiona i membri
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ Revisiona i membri prima di ammetterli ("bussare").
+ admission stage description
+
Revoke
Revoca
@@ -6562,6 +6654,11 @@ chat item action
Salva (e avvisa i contatti)
alert button
+
+ Save admission settings?
+ Salvare le impostazioni di ammissione?
+ alert title
+
Save and notify contact
Salva e avvisa il contatto
@@ -7077,6 +7174,11 @@ chat item action
Impostalo al posto dell'autenticazione di sistema.
No comment provided by engineer.
+
+ Set member admission
+ Imposta l'ammissione del membro
+ No comment provided by engineer.
+
Set message expiration in chats.
Imposta la scadenza dei messaggi nelle chat.
@@ -8839,6 +8941,11 @@ Ripetere la richiesta di ingresso?
Puoi vedere di nuovo il link di invito nei dettagli di connessione.
alert message
+
+ You can view your reports in Chat with admins.
+ Puoi vedere le tue segnalazioni nella chat con gli amministratori.
+ alert message
+
You can't send messages!
Non puoi inviare messaggi!
@@ -9141,6 +9248,11 @@ Ripetere la richiesta di connessione?
sopra, quindi scegli:
No comment provided by engineer.
+
+ accepted %@
+ %@ accettato
+ rcv group event chat item
+
accepted call
chiamata accettata
@@ -9151,6 +9263,11 @@ Ripetere la richiesta di connessione?
invito accettato
chat list item title
+
+ accepted you
+ ti ha accettato/a
+ rcv group event chat item
+
admin
amministratore
@@ -9171,6 +9288,11 @@ Ripetere la richiesta di connessione?
concordando la crittografia…
chat item text
+
+ all
+ tutti
+ member criteria value
+
all members
tutti i membri
@@ -9257,6 +9379,11 @@ marked deleted chat item preview text
chiamata…
call status
+
+ can't send messages
+ impossibile inviare messaggi
+ No comment provided by engineer.
+
cancelled %@
annullato %@
@@ -9362,6 +9489,16 @@ marked deleted chat item preview text
contatto %1$@ cambiato in %2$@
profile update event chat item
+
+ contact deleted
+ contatto eliminato
+ No comment provided by engineer.
+
+
+ contact disabled
+ contatto disattivato
+ No comment provided by engineer.
+
contact has e2e encryption
il contatto ha la crittografia e2e
@@ -9372,6 +9509,11 @@ marked deleted chat item preview text
il contatto non ha la crittografia e2e
No comment provided by engineer.
+
+ contact not ready
+ contatto non pronto
+ No comment provided by engineer.
+
creator
creatore
@@ -9543,6 +9685,11 @@ pref value
gruppo eliminato
No comment provided by engineer.
+
+ group is deleted
+ il gruppo è eliminato
+ No comment provided by engineer.
+
group profile updated
profilo del gruppo aggiornato
@@ -9668,6 +9815,11 @@ pref value
si è connesso/a
rcv group event chat item
+
+ member has old version
+ il membro ha una versione vecchia
+ No comment provided by engineer.
+
message
messaggio
@@ -9733,6 +9885,11 @@ pref value
nessun testo
copied message info in history
+
+ not synchronized
+ non sincronizzato
+ No comment provided by engineer.
+
observer
osservatore
@@ -9743,6 +9900,7 @@ pref value
off
enabled status
group pref value
+member criteria value
time to disappear
@@ -9795,6 +9953,11 @@ time to disappear
in attesa di approvazione
No comment provided by engineer.
+
+ pending review
+ in attesa di revisione
+ No comment provided by engineer.
+
quantum resistant e2e encryption
crittografia e2e resistente alla quantistica
@@ -9835,6 +9998,11 @@ time to disappear
indirizzo di contatto rimosso
profile update event chat item
+
+ removed from group
+ rimosso dal gruppo
+ No comment provided by engineer.
+
removed profile picture
immagine del profilo rimossa
@@ -9845,11 +10013,26 @@ time to disappear
ti ha rimosso/a
rcv group event chat item
+
+ request to join rejected
+ richiesta di entrare rifiutata
+ No comment provided by engineer.
+
requested to connect
richiesto di connettersi
chat list item title
+
+ review
+ revisiona
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ revisionato dagli amministratori
+ No comment provided by engineer.
+
saved
salvato
@@ -10039,6 +10222,11 @@ ultimo msg ricevuto: %2$@
tu
No comment provided by engineer.
+
+ you accepted this member
+ hai accettato questo membro
+ snd group event chat item
+
you are invited to group
sei stato/a invitato/a al gruppo
diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff
index 27134216a7..2a7bfa8df1 100644
--- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff
+++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff
@@ -561,6 +561,14 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ alert action
+
+
+ Accept as observer
+ alert action
+
Accept conditions
No comment provided by engineer.
@@ -581,6 +589,10 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ alert title
+
Accepted conditions
No comment provided by engineer.
@@ -1488,11 +1500,23 @@ set passcode view
Chat will be deleted for you - this cannot be undone!
No comment provided by engineer.
+
+ Chat with admins
+ chat toolbar
+
+
+ Chat with member
+ No comment provided by engineer.
+
Chats
チャット
No comment provided by engineer.
+
+ Chats with members
+ No comment provided by engineer.
+
Check messages every 20 min.
No comment provided by engineer.
@@ -2244,6 +2268,10 @@ swipe action
チャットのプロフィールを削除しますか?
No comment provided by engineer.
+
+ Delete chat with member?
+ alert title
+
Delete chat?
No comment provided by engineer.
@@ -2637,7 +2665,7 @@ swipe action
Don't show again
次から表示しない
- No comment provided by engineer.
+ alert action
Done
@@ -2931,6 +2959,10 @@ chat item action
連絡先リクエストの承諾にエラー発生
No comment provided by engineer.
+
+ Error accepting member
+ alert title
+
Error adding member(s)
メンバー追加にエラー発生
@@ -3018,6 +3050,10 @@ chat item action
チャットデータベース削除にエラー発生
No comment provided by engineer.
+
+ Error deleting chat with member
+ alert title
+
Error deleting chat!
チャット削除にエラー発生!
@@ -3117,7 +3153,7 @@ chat item action
Error removing member
メンバー除名にエラー発生
- No comment provided by engineer.
+ alert title
Error reordering lists
@@ -4384,6 +4420,10 @@ This is your link for group %@!
メンバー
No comment provided by engineer.
+
+ Member admission
+ No comment provided by engineer.
+
Member inactive
item status text
@@ -4415,6 +4455,10 @@ This is your link for group %@!
メンバーをグループから除名する (※元に戻せません※)!
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ alert message
+
Members can add message reactions.
グループメンバーはメッセージへのリアクションを追加できます。
@@ -4792,6 +4836,10 @@ This is your link for group %@!
新しいメンバーの役割
No comment provided by engineer.
+
+ New member wants to join the group.
+ rcv group event chat item
+
New message
新しいメッセージ
@@ -4828,6 +4876,10 @@ This is your link for group %@!
No chats in list %@
No comment provided by engineer.
+
+ No chats with members
+ No comment provided by engineer.
+
No contacts selected
連絡先が選択されてません
@@ -4998,7 +5050,8 @@ This is your link for group %@!
Ok
OK
- alert button
+ alert action
+alert button
Old database
@@ -5373,6 +5426,10 @@ Error: %@
Please try to disable and re-enable notfications.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ snd group event chat item
+
Please wait for token activation to complete.
token info
@@ -5784,6 +5841,10 @@ swipe action
連絡要求を拒否する
No comment provided by engineer.
+
+ Reject member?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
中継サーバーは必要な場合にのみ使用されます。 別の当事者があなたの IP アドレスを監視できます。
@@ -5882,6 +5943,10 @@ swipe action
Report reason?
No comment provided by engineer.
+
+ Report sent to moderators
+ alert title
+
Report spam: only group moderators will see it.
report reason
@@ -5985,6 +6050,14 @@ swipe action
Review conditions
No comment provided by engineer.
+
+ Review members
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ admission stage description
+
Revoke
取り消す
@@ -6037,6 +6110,10 @@ chat item action
保存(連絡先に通知)
alert button
+
+ Save admission settings?
+ alert title
+
Save and notify contact
保存して、連絡先にに知らせる
@@ -6500,6 +6577,10 @@ chat item action
システム認証の代わりに設定します。
No comment provided by engineer.
+
+ Set member admission
+ No comment provided by engineer.
+
Set message expiration in chats.
No comment provided by engineer.
@@ -8088,6 +8169,10 @@ Repeat join request?
You can view invitation link again in connection details.
alert message
+
+ You can view your reports in Chat with admins.
+ alert message
+
You can't send messages!
メッセージを送信できませんでした!
@@ -8374,6 +8459,10 @@ Repeat connection request?
上で選んでください:
No comment provided by engineer.
+
+ accepted %@
+ rcv group event chat item
+
accepted call
受けた通話
@@ -8383,6 +8472,10 @@ Repeat connection request?
accepted invitation
chat list item title
+
+ accepted you
+ rcv group event chat item
+
admin
管理者
@@ -8402,6 +8495,10 @@ Repeat connection request?
暗号化に同意しています…
chat item text
+
+ all
+ member criteria value
+
all members
feature role
@@ -8479,6 +8576,10 @@ marked deleted chat item preview text
発信中…
call status
+
+ can't send messages
+ No comment provided by engineer.
+
cancelled %@
キャンセルされました %@
@@ -8582,6 +8683,14 @@ marked deleted chat item preview text
contact %1$@ changed to %2$@
profile update event chat item
+
+ contact deleted
+ No comment provided by engineer.
+
+
+ contact disabled
+ No comment provided by engineer.
+
contact has e2e encryption
連絡先はエンドツーエンド暗号化があります
@@ -8592,6 +8701,10 @@ marked deleted chat item preview text
連絡先はエンドツーエンド暗号化がありません
No comment provided by engineer.
+
+ contact not ready
+ No comment provided by engineer.
+
creator
作成者
@@ -8758,6 +8871,10 @@ pref value
グループ削除済み
No comment provided by engineer.
+
+ group is deleted
+ No comment provided by engineer.
+
group profile updated
グループのプロフィールが更新されました
@@ -8880,6 +8997,10 @@ pref value
接続中
rcv group event chat item
+
+ member has old version
+ No comment provided by engineer.
+
message
No comment provided by engineer.
@@ -8943,6 +9064,10 @@ pref value
テキストなし
copied message info in history
+
+ not synchronized
+ No comment provided by engineer.
+
observer
オブザーバー
@@ -8953,6 +9078,7 @@ pref value
オフ
enabled status
group pref value
+member criteria value
time to disappear
@@ -9000,6 +9126,10 @@ time to disappear
pending approval
No comment provided by engineer.
+
+ pending review
+ No comment provided by engineer.
+
quantum resistant e2e encryption
chat item text
@@ -9037,6 +9167,10 @@ time to disappear
removed contact address
profile update event chat item
+
+ removed from group
+ No comment provided by engineer.
+
removed profile picture
profile update event chat item
@@ -9046,10 +9180,22 @@ time to disappear
あなたを除名しました
rcv group event chat item
+
+ request to join rejected
+ No comment provided by engineer.
+
requested to connect
chat list item title
+
+ review
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ No comment provided by engineer.
+
saved
No comment provided by engineer.
@@ -9220,6 +9366,10 @@ last received msg: %2$@
you
No comment provided by engineer.
+
+ you accepted this member
+ snd group event chat item
+
you are invited to group
グループ招待が届きました
diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff
index 4008c57ac0..d0b430cf02 100644
--- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff
+++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff
@@ -565,6 +565,14 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ alert action
+
+
+ Accept as observer
+ alert action
+
Accept conditions
Accepteer voorwaarden
@@ -586,6 +594,10 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ alert title
+
Accepted conditions
Geaccepteerde voorwaarden
@@ -1581,11 +1593,23 @@ set passcode view
De chat wordt voor je verwijderd - dit kan niet ongedaan worden gemaakt!
No comment provided by engineer.
+
+ Chat with admins
+ chat toolbar
+
+
+ Chat with member
+ No comment provided by engineer.
+
Chats
Chats
No comment provided by engineer.
+
+ Chats with members
+ No comment provided by engineer.
+
Check messages every 20 min.
Controleer uw berichten elke 20 minuten.
@@ -2401,6 +2425,10 @@ swipe action
Chatprofiel verwijderen?
No comment provided by engineer.
+
+ Delete chat with member?
+ alert title
+
Delete chat?
Chat verwijderen?
@@ -2824,7 +2852,7 @@ swipe action
Don't show again
Niet meer weergeven
- No comment provided by engineer.
+ alert action
Done
@@ -3142,6 +3170,10 @@ chat item action
Fout bij het accepteren van een contactverzoek
No comment provided by engineer.
+
+ Error accepting member
+ alert title
+
Error adding member(s)
Fout bij het toevoegen van leden
@@ -3237,6 +3269,10 @@ chat item action
Fout bij het verwijderen van de chat database
No comment provided by engineer.
+
+ Error deleting chat with member
+ alert title
+
Error deleting chat!
Fout bij verwijderen gesprek!
@@ -3345,7 +3381,7 @@ chat item action
Error removing member
Fout bij verwijderen van lid
- No comment provided by engineer.
+ alert title
Error reordering lists
@@ -4729,6 +4765,10 @@ Dit is jouw link voor groep %@!
Lid
No comment provided by engineer.
+
+ Member admission
+ No comment provided by engineer.
+
Member inactive
Lid inactief
@@ -4764,6 +4804,10 @@ Dit is jouw link voor groep %@!
Lid wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt!
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ alert message
+
Members can add message reactions.
Groepsleden kunnen bericht reacties toevoegen.
@@ -5184,6 +5228,10 @@ Dit is jouw link voor groep %@!
Nieuwe leden rol
No comment provided by engineer.
+
+ New member wants to join the group.
+ rcv group event chat item
+
New message
nieuw bericht
@@ -5224,6 +5272,10 @@ Dit is jouw link voor groep %@!
Geen chats in lijst %@
No comment provided by engineer.
+
+ No chats with members
+ No comment provided by engineer.
+
No contacts selected
Geen contacten geselecteerd
@@ -5416,7 +5468,8 @@ Dit is jouw link voor groep %@!
Ok
OK
- alert button
+ alert action
+alert button
Old database
@@ -5827,6 +5880,10 @@ Fout: %@
Probeer meldingen uit en weer in te schakelen.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ snd group event chat item
+
Please wait for token activation to complete.
Wacht tot de tokenactivering voltooid is.
@@ -6280,6 +6337,10 @@ swipe action
Contactverzoek afwijzen
No comment provided by engineer.
+
+ Reject member?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
Relay server wordt alleen gebruikt als dat nodig is. Een andere partij kan uw IP-adres zien.
@@ -6390,6 +6451,10 @@ swipe action
Reden melding?
No comment provided by engineer.
+
+ Report sent to moderators
+ alert title
+
Report spam: only group moderators will see it.
Spam melden: alleen groepsmoderators kunnen het zien.
@@ -6505,6 +6570,14 @@ swipe action
Voorwaarden bekijken
No comment provided by engineer.
+
+ Review members
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ admission stage description
+
Revoke
Intrekken
@@ -6561,6 +6634,10 @@ chat item action
Bewaar (en informeer contacten)
alert button
+
+ Save admission settings?
+ alert title
+
Save and notify contact
Opslaan en Contact melden
@@ -7076,6 +7153,10 @@ chat item action
Stel het in in plaats van systeemverificatie.
No comment provided by engineer.
+
+ Set member admission
+ No comment provided by engineer.
+
Set message expiration in chats.
Stel de berichtvervaldatum in chats in.
@@ -8832,6 +8913,10 @@ Deelnameverzoek herhalen?
U kunt de uitnodigingslink opnieuw bekijken in de verbindingsdetails.
alert message
+
+ You can view your reports in Chat with admins.
+ alert message
+
You can't send messages!
Je kunt geen berichten versturen!
@@ -9134,6 +9219,10 @@ Verbindingsverzoek herhalen?
hier boven, kies dan:
No comment provided by engineer.
+
+ accepted %@
+ rcv group event chat item
+
accepted call
geaccepteerde oproep
@@ -9144,6 +9233,10 @@ Verbindingsverzoek herhalen?
geaccepteerde uitnodiging
chat list item title
+
+ accepted you
+ rcv group event chat item
+
admin
Beheerder
@@ -9164,6 +9257,10 @@ Verbindingsverzoek herhalen?
versleuteling overeenkomen…
chat item text
+
+ all
+ member criteria value
+
all members
alle leden
@@ -9250,6 +9347,10 @@ marked deleted chat item preview text
bellen…
call status
+
+ can't send messages
+ No comment provided by engineer.
+
cancelled %@
geannuleerd %@
@@ -9355,6 +9456,14 @@ marked deleted chat item preview text
contactpersoon %1$@ gewijzigd in %2$@
profile update event chat item
+
+ contact deleted
+ No comment provided by engineer.
+
+
+ contact disabled
+ No comment provided by engineer.
+
contact has e2e encryption
contact heeft e2e-codering
@@ -9365,6 +9474,10 @@ marked deleted chat item preview text
contact heeft geen e2e versleuteling
No comment provided by engineer.
+
+ contact not ready
+ No comment provided by engineer.
+
creator
creator
@@ -9536,6 +9649,10 @@ pref value
groep verwijderd
No comment provided by engineer.
+
+ group is deleted
+ No comment provided by engineer.
+
group profile updated
groep profiel bijgewerkt
@@ -9661,6 +9778,10 @@ pref value
is toegetreden
rcv group event chat item
+
+ member has old version
+ No comment provided by engineer.
+
message
bericht
@@ -9726,6 +9847,10 @@ pref value
geen tekst
copied message info in history
+
+ not synchronized
+ No comment provided by engineer.
+
observer
Waarnemer
@@ -9736,6 +9861,7 @@ pref value
uit
enabled status
group pref value
+member criteria value
time to disappear
@@ -9788,6 +9914,10 @@ time to disappear
in afwachting van goedkeuring
No comment provided by engineer.
+
+ pending review
+ No comment provided by engineer.
+
quantum resistant e2e encryption
quantum bestendige e2e-codering
@@ -9828,6 +9958,10 @@ time to disappear
contactadres verwijderd
profile update event chat item
+
+ removed from group
+ No comment provided by engineer.
+
removed profile picture
profielfoto verwijderd
@@ -9838,11 +9972,23 @@ time to disappear
heeft je verwijderd
rcv group event chat item
+
+ request to join rejected
+ No comment provided by engineer.
+
requested to connect
verzocht om verbinding te maken
chat list item title
+
+ review
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ No comment provided by engineer.
+
saved
opgeslagen
@@ -10032,6 +10178,10 @@ laatst ontvangen bericht: %2$@
jij
No comment provided by engineer.
+
+ you accepted this member
+ snd group event chat item
+
you are invited to group
je bent uitgenodigd voor de groep
diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff
index 175c8b4112..3255489efd 100644
--- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff
+++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff
@@ -565,6 +565,14 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ alert action
+
+
+ Accept as observer
+ alert action
+
Accept conditions
Zaakceptuj warunki
@@ -586,6 +594,10 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ alert title
+
Accepted conditions
Zaakceptowano warunki
@@ -1575,11 +1587,23 @@ set passcode view
Czat zostanie usunięty dla Ciebie – tej operacji nie można cofnąć!
No comment provided by engineer.
+
+ Chat with admins
+ chat toolbar
+
+
+ Chat with member
+ No comment provided by engineer.
+
Chats
Czaty
No comment provided by engineer.
+
+ Chats with members
+ No comment provided by engineer.
+
Check messages every 20 min.
Sprawdzaj wiadomości co 20 min.
@@ -2374,6 +2398,10 @@ swipe action
Usunąć profil czatu?
No comment provided by engineer.
+
+ Delete chat with member?
+ alert title
+
Delete chat?
No comment provided by engineer.
@@ -2787,7 +2815,7 @@ swipe action
Don't show again
Nie pokazuj ponownie
- No comment provided by engineer.
+ alert action
Done
@@ -3100,6 +3128,10 @@ chat item action
Błąd przyjmowania prośby o kontakt
No comment provided by engineer.
+
+ Error accepting member
+ alert title
+
Error adding member(s)
Błąd dodawania członka(ów)
@@ -3191,6 +3223,10 @@ chat item action
Błąd usuwania bazy danych czatu
No comment provided by engineer.
+
+ Error deleting chat with member
+ alert title
+
Error deleting chat!
Błąd usuwania czatu!
@@ -3297,7 +3333,7 @@ chat item action
Error removing member
Błąd usuwania członka
- No comment provided by engineer.
+ alert title
Error reordering lists
@@ -4641,6 +4677,10 @@ To jest twój link do grupy %@!
Członek
No comment provided by engineer.
+
+ Member admission
+ No comment provided by engineer.
+
Member inactive
Członek nieaktywny
@@ -4673,6 +4713,10 @@ To jest twój link do grupy %@!
Członek zostanie usunięty z grupy - nie można tego cofnąć!
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ alert message
+
Members can add message reactions.
Członkowie grupy mogą dodawać reakcje wiadomości.
@@ -5083,6 +5127,10 @@ To jest twój link do grupy %@!
Nowa rola członka
No comment provided by engineer.
+
+ New member wants to join the group.
+ rcv group event chat item
+
New message
Nowa wiadomość
@@ -5119,6 +5167,10 @@ To jest twój link do grupy %@!
No chats in list %@
No comment provided by engineer.
+
+ No chats with members
+ No comment provided by engineer.
+
No contacts selected
Nie wybrano kontaktów
@@ -5298,7 +5350,8 @@ To jest twój link do grupy %@!
Ok
Ok
- alert button
+ alert action
+alert button
Old database
@@ -5698,6 +5751,10 @@ Błąd: %@
Please try to disable and re-enable notfications.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ snd group event chat item
+
Please wait for token activation to complete.
token info
@@ -6140,6 +6197,10 @@ swipe action
Odrzuć prośbę kontaktu
No comment provided by engineer.
+
+ Reject member?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
Serwer przekaźnikowy jest używany tylko w razie potrzeby. Inna strona może obserwować Twój adres IP.
@@ -6245,6 +6306,10 @@ swipe action
Report reason?
No comment provided by engineer.
+
+ Report sent to moderators
+ alert title
+
Report spam: only group moderators will see it.
report reason
@@ -6354,6 +6419,14 @@ swipe action
Review conditions
No comment provided by engineer.
+
+ Review members
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ admission stage description
+
Revoke
Odwołaj
@@ -6410,6 +6483,10 @@ chat item action
Zapisz (i powiadom kontakty)
alert button
+
+ Save admission settings?
+ alert title
+
Save and notify contact
Zapisz i powiadom kontakt
@@ -6918,6 +6995,10 @@ chat item action
Ustaw go zamiast uwierzytelniania systemowego.
No comment provided by engineer.
+
+ Set member admission
+ No comment provided by engineer.
+
Set message expiration in chats.
No comment provided by engineer.
@@ -8630,6 +8711,10 @@ Powtórzyć prośbę dołączenia?
Możesz zobaczyć link zaproszenia ponownie w szczegółach połączenia.
alert message
+
+ You can view your reports in Chat with admins.
+ alert message
+
You can't send messages!
Nie możesz wysyłać wiadomości!
@@ -8930,6 +9015,10 @@ Powtórzyć prośbę połączenia?
powyżej, a następnie wybierz:
No comment provided by engineer.
+
+ accepted %@
+ rcv group event chat item
+
accepted call
zaakceptowane połączenie
@@ -8939,6 +9028,10 @@ Powtórzyć prośbę połączenia?
accepted invitation
chat list item title
+
+ accepted you
+ rcv group event chat item
+
admin
administrator
@@ -8959,6 +9052,10 @@ Powtórzyć prośbę połączenia?
uzgadnianie szyfrowania…
chat item text
+
+ all
+ member criteria value
+
all members
wszyscy członkowie
@@ -9044,6 +9141,10 @@ marked deleted chat item preview text
dzwonie…
call status
+
+ can't send messages
+ No comment provided by engineer.
+
cancelled %@
anulowany %@
@@ -9149,6 +9250,14 @@ marked deleted chat item preview text
kontakt %1$@ zmieniony na %2$@
profile update event chat item
+
+ contact deleted
+ No comment provided by engineer.
+
+
+ contact disabled
+ No comment provided by engineer.
+
contact has e2e encryption
kontakt posiada szyfrowanie e2e
@@ -9159,6 +9268,10 @@ marked deleted chat item preview text
kontakt nie posiada szyfrowania e2e
No comment provided by engineer.
+
+ contact not ready
+ No comment provided by engineer.
+
creator
twórca
@@ -9330,6 +9443,10 @@ pref value
grupa usunięta
No comment provided by engineer.
+
+ group is deleted
+ No comment provided by engineer.
+
group profile updated
zaktualizowano profil grupy
@@ -9455,6 +9572,10 @@ pref value
połączony
rcv group event chat item
+
+ member has old version
+ No comment provided by engineer.
+
message
wiadomość
@@ -9519,6 +9640,10 @@ pref value
brak tekstu
copied message info in history
+
+ not synchronized
+ No comment provided by engineer.
+
observer
obserwator
@@ -9529,6 +9654,7 @@ pref value
wyłączony
enabled status
group pref value
+member criteria value
time to disappear
@@ -9579,6 +9705,10 @@ time to disappear
pending approval
No comment provided by engineer.
+
+ pending review
+ No comment provided by engineer.
+
quantum resistant e2e encryption
kwantowo odporne szyfrowanie e2e
@@ -9618,6 +9748,10 @@ time to disappear
usunięto adres kontaktu
profile update event chat item
+
+ removed from group
+ No comment provided by engineer.
+
removed profile picture
usunięto zdjęcie profilu
@@ -9628,10 +9762,22 @@ time to disappear
usunął cię
rcv group event chat item
+
+ request to join rejected
+ No comment provided by engineer.
+
requested to connect
chat list item title
+
+ review
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ No comment provided by engineer.
+
saved
zapisane
@@ -9821,6 +9967,10 @@ ostatnia otrzymana wiadomość: %2$@
Ty
No comment provided by engineer.
+
+ you accepted this member
+ snd group event chat item
+
you are invited to group
jesteś zaproszony do grupy
diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff
index 419fa75375..2ec1130718 100644
--- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff
+++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff
@@ -167,7 +167,7 @@
%d hours
- %d ч.
+ %d час.
time interval
@@ -565,6 +565,16 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ Принять в группу
+ alert action
+
+
+ Accept as observer
+ Принять как читателя
+ alert action
+
Accept conditions
Принять условия
@@ -586,6 +596,11 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ Принять члена
+ alert title
+
Accepted conditions
Принятые условия
@@ -798,6 +813,7 @@ swipe action
All servers
+ Все серверы
No comment provided by engineer.
@@ -1581,11 +1597,26 @@ set passcode view
Разговор будет удален для Вас - это действие нельзя отменить!
No comment provided by engineer.
+
+ Chat with admins
+ Чат с админами
+ chat toolbar
+
+
+ Chat with member
+ Чат с членом группы
+ No comment provided by engineer.
+
Chats
Чаты
No comment provided by engineer.
+
+ Chats with members
+ Чаты с членами группы
+ No comment provided by engineer.
+
Check messages every 20 min.
Проверять сообщения каждые 20 минут.
@@ -2401,6 +2432,11 @@ swipe action
Удалить профиль?
No comment provided by engineer.
+
+ Delete chat with member?
+ Удалить чат с членом группы?
+ alert title
+
Delete chat?
Удалить разговор?
@@ -2688,6 +2724,7 @@ swipe action
Direct messages between members are prohibited.
+ Прямые сообщения между членами запрещены.
No comment provided by engineer.
@@ -2792,6 +2829,7 @@ swipe action
Do not send history to new members.
+ Не отправлять историю новым членам.
No comment provided by engineer.
@@ -2822,7 +2860,7 @@ swipe action
Don't show again
Не показывать
- No comment provided by engineer.
+ alert action
Done
@@ -2917,6 +2955,7 @@ chat item action
Enable Flux in Network & servers settings for better metadata privacy.
+ Включите Flux в настройках Сеть и серверы для лучшей конфиденциальности метаданных.
No comment provided by engineer.
@@ -3139,8 +3178,14 @@ chat item action
Ошибка при принятии запроса на соединение
No comment provided by engineer.
+
+ Error accepting member
+ Ошибка вступления члена группы
+ alert title
+
Error adding member(s)
+ Ошибка при добавлении членов группы
No comment provided by engineer.
@@ -3205,6 +3250,7 @@ chat item action
Error creating member contact
+ Ошибка при создании контакта
No comment provided by engineer.
@@ -3232,6 +3278,11 @@ chat item action
Ошибка при удалении данных чата
No comment provided by engineer.
+
+ Error deleting chat with member
+ Ошибка при удалении чата с членом группы
+ alert title
+
Error deleting chat!
Ошибка при удалении чата!
@@ -3339,7 +3390,8 @@ chat item action
Error removing member
- No comment provided by engineer.
+ Ошибка при удалении члена группы
+ alert title
Error reordering lists
@@ -3403,6 +3455,7 @@ chat item action
Error sending member contact invitation
+ Ошибка при отправке приглашения члену
No comment provided by engineer.
@@ -3738,6 +3791,7 @@ snd error text
Fix not supported by group member
+ Починка не поддерживается членом группы.
No comment provided by engineer.
@@ -3871,6 +3925,7 @@ Error: %2$@
Fully decentralized – visible only to members.
+ Группа полностью децентрализована – она видна только членам.
No comment provided by engineer.
@@ -3980,6 +4035,7 @@ Error: %2$@
Group profile is stored on members' devices, not on the servers.
+ Профиль группы хранится на устройствах членов, а не на серверах.
No comment provided by engineer.
@@ -3989,6 +4045,7 @@ Error: %2$@
Group will be deleted for all members - this cannot be undone!
+ Группа будет удалена для всех членов - это действие нельзя отменить!
No comment provided by engineer.
@@ -4053,6 +4110,7 @@ Error: %2$@
History is not sent to new members.
+ История не отправляется новым членам.
No comment provided by engineer.
@@ -4400,6 +4458,7 @@ More improvements are coming soon!
Invite members
+ Пригласить членов группы
No comment provided by engineer.
@@ -4712,10 +4771,17 @@ This is your link for group %@!
Member
+ Член группы
+ No comment provided by engineer.
+
+
+ Member admission
+ Приём членов в группу
No comment provided by engineer.
Member inactive
+ Член неактивен
item status text
@@ -4730,54 +4796,72 @@ This is your link for group %@!
Member role will be changed to "%@". All group members will be notified.
+ Роль члена будет изменена на "%@". Все члены группы получат уведомление.
No comment provided by engineer.
Member role will be changed to "%@". The member will receive a new invitation.
+ Роль члена будет изменена на "%@". Будет отправлено новое приглашение.
No comment provided by engineer.
Member will be removed from chat - this cannot be undone!
+ Член будет удален из разговора - это действие нельзя отменить!
No comment provided by engineer.
Member will be removed from group - this cannot be undone!
+ Член группы будет удален - это действие нельзя отменить!
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ Участник хочет присоединиться к группе. Принять?
+ alert message
+
Members can add message reactions.
+ Члены могут добавлять реакции на сообщения.
No comment provided by engineer.
Members can irreversibly delete sent messages. (24 hours)
+ Члены могут необратимо удалять отправленные сообщения. (24 часа)
No comment provided by engineer.
Members can report messsages to moderators.
+ Члены группы могут пожаловаться модераторам.
No comment provided by engineer.
Members can send SimpleX links.
+ Члены могут отправлять ссылки SimpleX.
No comment provided by engineer.
Members can send direct messages.
+ Члены могут посылать прямые сообщения.
No comment provided by engineer.
Members can send disappearing messages.
+ Члены могут посылать исчезающие сообщения.
No comment provided by engineer.
Members can send files and media.
+ Члены могут слать файлы и медиа.
No comment provided by engineer.
Members can send voice messages.
+ Члены могут отправлять голосовые сообщения.
No comment provided by engineer.
Mention members 👋
+ Упоминайте участников 👋
No comment provided by engineer.
Message may be delivered later if member becomes active.
+ Сообщение может быть доставлено позже, если член группы станет активным.
item status description
@@ -5151,8 +5236,14 @@ This is your link for group %@!
New member role
+ Роль члена группы
No comment provided by engineer.
+
+ New member wants to join the group.
+ Новый участник хочет присоединиться к группе.
+ rcv group event chat item
+
New message
Новое сообщение
@@ -5193,6 +5284,11 @@ This is your link for group %@!
Нет чатов в списке %@
No comment provided by engineer.
+
+ No chats with members
+ Нет чатов с членами группы
+ No comment provided by engineer.
+
No contacts selected
Контакты не выбраны
@@ -5367,6 +5463,9 @@ This is your link for group %@!
Now admins can:
- delete members' messages.
- disable members ("observer" role)
+ Теперь админы могут:
+- удалять сообщения членов.
+- приостанавливать членов (роль наблюдатель)
No comment provided by engineer.
@@ -5382,7 +5481,8 @@ This is your link for group %@!
Ok
Ок
- alert button
+ alert action
+alert button
Old database
@@ -5540,6 +5640,7 @@ Requires compatible VPN.
Open link?
+ Открыть ссылку?
alert title
@@ -5656,6 +5757,7 @@ Requires compatible VPN.
Past member %@
+ Бывший член %@
past/unknown group member
@@ -5792,6 +5894,11 @@ Error: %@
Попробуйте выключить и снова включить уведомления.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ Пожалуйста, подождите, пока модераторы группы рассмотрят ваш запрос на вступление.
+ snd group event chat item
+
Please wait for token activation to complete.
Пожалуйста, дождитесь завершения активации токена.
@@ -5964,6 +6071,7 @@ Error: %@
Prohibit sending direct messages to members.
+ Запретить посылать прямые сообщения членам группы.
No comment provided by engineer.
@@ -6244,6 +6352,11 @@ swipe action
Отклонить запрос
No comment provided by engineer.
+
+ Reject member?
+ Отклонить участника?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
Relay сервер используется только при необходимости. Другая сторона может видеть Ваш IP адрес.
@@ -6271,10 +6384,12 @@ swipe action
Remove member
+ Удалить члена группы
No comment provided by engineer.
Remove member?
+ Удалить члена группы?
No comment provided by engineer.
@@ -6352,6 +6467,11 @@ swipe action
Причина сообщения?
No comment provided by engineer.
+
+ Report sent to moderators
+ Жалоба отправлена модераторам
+ alert title
+
Report spam: only group moderators will see it.
Пожаловаться на спам: увидят только модераторы группы.
@@ -6467,6 +6587,16 @@ swipe action
Посмотреть условия
No comment provided by engineer.
+
+ Review members
+ Одобрять членов
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ Одобрять членов для вступления в группу.
+ admission stage description
+
Revoke
Отозвать
@@ -6523,6 +6653,11 @@ chat item action
Сохранить (и уведомить контакты)
alert button
+
+ Save admission settings?
+ Сохранить настройки вступления?
+ alert title
+
Save and notify contact
Сохранить и уведомить контакт
@@ -6530,6 +6665,7 @@ chat item action
Save and notify group members
+ Сохранить и уведомить членов группы
No comment provided by engineer.
@@ -6814,6 +6950,7 @@ chat item action
Send up to 100 last messages to new members.
+ Отправить до 100 последних сообщений новым членам.
No comment provided by engineer.
@@ -7036,6 +7173,11 @@ chat item action
Установите код вместо системной аутентификации.
No comment provided by engineer.
+
+ Set member admission
+ Приём членов в группу
+ No comment provided by engineer.
+
Set message expiration in chats.
Установите срок хранения сообщений в чатах.
@@ -7058,6 +7200,7 @@ chat item action
Set the message shown to new members!
+ Установить сообщение для новых членов группы!
No comment provided by engineer.
@@ -7148,6 +7291,7 @@ chat item action
Short link
+ Короткая ссылка
No comment provided by engineer.
@@ -7252,6 +7396,7 @@ chat item action
SimpleX channel link
+ SimpleX ссылка канала
simplex link type
@@ -7696,18 +7841,22 @@ It can happen because of some bug or when the connection is compromised.
The message will be deleted for all members.
+ Сообщение будет удалено для всех членов группы.
No comment provided by engineer.
The message will be marked as moderated for all members.
+ Сообщение будет помечено как удаленное для всех членов группы.
No comment provided by engineer.
The messages will be deleted for all members.
+ Сообщения будут удалены для всех членов группы.
No comment provided by engineer.
The messages will be marked as moderated for all members.
+ Сообщения будут помечены как удаленные для всех членов группы.
No comment provided by engineer.
@@ -7817,6 +7966,7 @@ It can happen because of some bug or when the connection is compromised.
This group has over %lld members, delivery receipts are not sent.
+ В этой группе более %lld членов, отчёты о доставке не отправляются.
No comment provided by engineer.
@@ -7836,6 +7986,7 @@ It can happen because of some bug or when the connection is compromised.
This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.
+ Эта ссылка требует новую версию. Обновите приложение или попросите Ваш контакт прислать совместимую ссылку.
No comment provided by engineer.
@@ -8027,14 +8178,17 @@ You will be prompted to complete authentication before this feature is enabled.<
Unblock member
+ Разблокировать члена группы
No comment provided by engineer.
Unblock member for all?
+ Разблокировать члена для всех?
No comment provided by engineer.
Unblock member?
+ Разблокировать члена группы?
No comment provided by engineer.
@@ -8136,10 +8290,12 @@ To connect, please ask your contact to create another connection link and check
Unsupported connection link
+ Ссылка не поддерживается
No comment provided by engineer.
Up to 100 last messages are sent to new members.
+ До 100 последних сообщений отправляются новым членам.
No comment provided by engineer.
@@ -8234,6 +8390,7 @@ To connect, please ask your contact to create another connection link and check
Use TCP port 443 for preset servers only.
+ Использовать TCP-порт 443 только для серверов по умолчанию.
No comment provided by engineer.
@@ -8303,6 +8460,7 @@ To connect, please ask your contact to create another connection link and check
Use short links (BETA)
+ Короткие ссылки (БЕТА)
No comment provided by engineer.
@@ -8749,6 +8907,7 @@ Repeat join request?
You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.
+ Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились.
No comment provided by engineer.
@@ -8781,6 +8940,11 @@ Repeat join request?
Вы можете увидеть ссылку-приглашение снова открыв соединение.
alert message
+
+ You can view your reports in Chat with admins.
+ Вы можете найти Ваши жалобы в Чате с админами.
+ alert message
+
You can't send messages!
Вы не можете отправлять сообщения!
@@ -8825,6 +8989,7 @@ Repeat connection request?
You joined this group. Connecting to inviting group member.
+ Вы вступили в эту группу. Устанавливается соединение с пригласившим членом группы.
No comment provided by engineer.
@@ -8894,6 +9059,7 @@ Repeat connection request?
You will connect to all group members.
+ Вы соединитесь со всеми членами группы.
No comment provided by engineer.
@@ -9081,6 +9247,11 @@ Repeat connection request?
наверху, затем выберите:
No comment provided by engineer.
+
+ accepted %@
+ принят %@
+ rcv group event chat item
+
accepted call
принятый звонок
@@ -9091,6 +9262,11 @@ Repeat connection request?
принятое приглашение
chat list item title
+
+ accepted you
+ Вы приняты
+ rcv group event chat item
+
admin
админ
@@ -9111,8 +9287,14 @@ Repeat connection request?
шифрование согласовывается…
chat item text
+
+ all
+ все
+ member criteria value
+
all members
+ все члены
feature role
@@ -9196,6 +9378,11 @@ marked deleted chat item preview text
входящий звонок…
call status
+
+ can't send messages
+ нельзя отправлять
+ No comment provided by engineer.
+
cancelled %@
отменил(a) %@
@@ -9301,6 +9488,16 @@ marked deleted chat item preview text
контакт %1$@ изменён на %2$@
profile update event chat item
+
+ contact deleted
+ контакт удален
+ No comment provided by engineer.
+
+
+ contact disabled
+ контакт выключен
+ No comment provided by engineer.
+
contact has e2e encryption
у контакта есть e2e шифрование
@@ -9311,6 +9508,11 @@ marked deleted chat item preview text
у контакта нет e2e шифрования
No comment provided by engineer.
+
+ contact not ready
+ контакт не готов
+ No comment provided by engineer.
+
creator
создатель
@@ -9482,6 +9684,11 @@ pref value
группа удалена
No comment provided by engineer.
+
+ group is deleted
+ группа удалена
+ No comment provided by engineer.
+
group profile updated
профиль группы обновлен
@@ -9594,10 +9801,12 @@ pref value
member
+ член группы
member role
member %1$@ changed to %2$@
+ член %1$@ изменился на %2$@
profile update event chat item
@@ -9605,6 +9814,11 @@ pref value
соединен(а)
rcv group event chat item
+
+ member has old version
+ член имеет старую версию
+ No comment provided by engineer.
+
message
написать
@@ -9670,6 +9884,11 @@ pref value
нет текста
copied message info in history
+
+ not synchronized
+ не синхронизирован
+ No comment provided by engineer.
+
observer
читатель
@@ -9680,6 +9899,7 @@ pref value
нет
enabled status
group pref value
+member criteria value
time to disappear
@@ -9732,6 +9952,11 @@ time to disappear
ожидает утверждения
No comment provided by engineer.
+
+ pending review
+ ожидает одобрения
+ No comment provided by engineer.
+
quantum resistant e2e encryption
квантово-устойчивое e2e шифрование
@@ -9772,6 +9997,11 @@ time to disappear
удалён адрес контакта
profile update event chat item
+
+ removed from group
+ удален из группы
+ No comment provided by engineer.
+
removed profile picture
удалена картинка профиля
@@ -9782,11 +10012,26 @@ time to disappear
удалил(а) Вас из группы
rcv group event chat item
+
+ request to join rejected
+ запрос на вступление отклонён
+ No comment provided by engineer.
+
requested to connect
запрошено соединение
chat list item title
+
+ review
+ рассмотрение
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ одобрен админами
+ No comment provided by engineer.
+
saved
сохранено
@@ -9976,6 +10221,11 @@ last received msg: %2$@
Вы
No comment provided by engineer.
+
+ you accepted this member
+ Вы приняли этого члена
+ snd group event chat item
+
you are invited to group
Вы приглашены в группу
@@ -10119,6 +10369,7 @@ last received msg: %2$@
From %d chat(s)
+ Из %d чатов
notification body
diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff
index 671dd87d7d..528219b13a 100644
--- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff
+++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff
@@ -520,6 +520,14 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ alert action
+
+
+ Accept as observer
+ alert action
+
Accept conditions
No comment provided by engineer.
@@ -539,6 +547,10 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ alert title
+
Accepted conditions
No comment provided by engineer.
@@ -1429,11 +1441,23 @@ set passcode view
Chat will be deleted for you - this cannot be undone!
No comment provided by engineer.
+
+ Chat with admins
+ chat toolbar
+
+
+ Chat with member
+ No comment provided by engineer.
+
Chats
แชท
No comment provided by engineer.
+
+ Chats with members
+ No comment provided by engineer.
+
Check messages every 20 min.
No comment provided by engineer.
@@ -2162,6 +2186,10 @@ swipe action
ลบโปรไฟล์แชทไหม?
No comment provided by engineer.
+
+ Delete chat with member?
+ alert title
+
Delete chat?
No comment provided by engineer.
@@ -2551,7 +2579,7 @@ swipe action
Don't show again
ไม่ต้องแสดงอีก
- No comment provided by engineer.
+ alert action
Done
@@ -2843,6 +2871,10 @@ chat item action
เกิดข้อผิดพลาดในการรับคำขอติดต่อ
No comment provided by engineer.
+
+ Error accepting member
+ alert title
+
Error adding member(s)
เกิดข้อผิดพลาดในการเพิ่มสมาชิก
@@ -2928,6 +2960,10 @@ chat item action
เกิดข้อผิดพลาดในการลบฐานข้อมูลแชท
No comment provided by engineer.
+
+ Error deleting chat with member
+ alert title
+
Error deleting chat!
เกิดข้อผิดพลาดในการลบแชท!
@@ -3028,7 +3064,7 @@ chat item action
Error removing member
เกิดข้อผิดพลาดในการลบสมาชิก
- No comment provided by engineer.
+ alert title
Error reordering lists
@@ -4293,6 +4329,10 @@ This is your link for group %@!
สมาชิก
No comment provided by engineer.
+
+ Member admission
+ No comment provided by engineer.
+
Member inactive
item status text
@@ -4324,6 +4364,10 @@ This is your link for group %@!
สมาชิกจะถูกลบออกจากกลุ่ม - ไม่สามารถยกเลิกได้!
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ alert message
+
Members can add message reactions.
สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้
@@ -4697,6 +4741,10 @@ This is your link for group %@!
บทบาทของสมาชิกใหม่
No comment provided by engineer.
+
+ New member wants to join the group.
+ rcv group event chat item
+
New message
ข้อความใหม่
@@ -4733,6 +4781,10 @@ This is your link for group %@!
No chats in list %@
No comment provided by engineer.
+
+ No chats with members
+ No comment provided by engineer.
+
No contacts selected
ไม่ได้เลือกผู้ติดต่อ
@@ -4902,7 +4954,8 @@ This is your link for group %@!
Ok
ตกลง
- alert button
+ alert action
+alert button
Old database
@@ -5274,6 +5327,10 @@ Error: %@
Please try to disable and re-enable notfications.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ snd group event chat item
+
Please wait for token activation to complete.
token info
@@ -5683,6 +5740,10 @@ swipe action
ปฏิเสธคำขอติดต่อ
No comment provided by engineer.
+
+ Reject member?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
ใช้เซิร์ฟเวอร์รีเลย์ในกรณีที่จำเป็นเท่านั้น บุคคลอื่นสามารถสังเกตที่อยู่ IP ของคุณได้
@@ -5781,6 +5842,10 @@ swipe action
Report reason?
No comment provided by engineer.
+
+ Report sent to moderators
+ alert title
+
Report spam: only group moderators will see it.
report reason
@@ -5884,6 +5949,14 @@ swipe action
Review conditions
No comment provided by engineer.
+
+ Review members
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ admission stage description
+
Revoke
ถอน
@@ -5936,6 +6009,10 @@ chat item action
บันทึก (และแจ้งผู้ติดต่อ)
alert button
+
+ Save admission settings?
+ alert title
+
Save and notify contact
บันทึกและแจ้งผู้ติดต่อ
@@ -6404,6 +6481,10 @@ chat item action
ตั้งแทนการรับรองความถูกต้องของระบบ
No comment provided by engineer.
+
+ Set member admission
+ No comment provided by engineer.
+
Set message expiration in chats.
No comment provided by engineer.
@@ -7986,6 +8067,10 @@ Repeat join request?
You can view invitation link again in connection details.
alert message
+
+ You can view your reports in Chat with admins.
+ alert message
+
You can't send messages!
คุณไม่สามารถส่งข้อความได้!
@@ -8270,6 +8355,10 @@ Repeat connection request?
ด้านบน จากนั้นเลือก:
No comment provided by engineer.
+
+ accepted %@
+ rcv group event chat item
+
accepted call
รับสายแล้ว
@@ -8279,6 +8368,10 @@ Repeat connection request?
accepted invitation
chat list item title
+
+ accepted you
+ rcv group event chat item
+
admin
ผู้ดูแลระบบ
@@ -8298,6 +8391,10 @@ Repeat connection request?
เห็นด้วยกับการ encryption…
chat item text
+
+ all
+ member criteria value
+
all members
feature role
@@ -8375,6 +8472,10 @@ marked deleted chat item preview text
กำลังโทร…
call status
+
+ can't send messages
+ No comment provided by engineer.
+
cancelled %@
ยกเลิก %@
@@ -8478,6 +8579,14 @@ marked deleted chat item preview text
contact %1$@ changed to %2$@
profile update event chat item
+
+ contact deleted
+ No comment provided by engineer.
+
+
+ contact disabled
+ No comment provided by engineer.
+
contact has e2e encryption
ผู้ติดต่อมีการ encrypt จากต้นจนจบ
@@ -8488,6 +8597,10 @@ marked deleted chat item preview text
ผู้ติดต่อไม่มีการ encrypt จากต้นจนจบ
No comment provided by engineer.
+
+ contact not ready
+ No comment provided by engineer.
+
creator
ผู้สร้าง
@@ -8653,6 +8766,10 @@ pref value
ลบกลุ่มแล้ว
No comment provided by engineer.
+
+ group is deleted
+ No comment provided by engineer.
+
group profile updated
อัปเดตโปรไฟล์กลุ่มแล้ว
@@ -8775,6 +8892,10 @@ pref value
เชื่อมต่อสำเร็จ
rcv group event chat item
+
+ member has old version
+ No comment provided by engineer.
+
message
No comment provided by engineer.
@@ -8838,6 +8959,10 @@ pref value
ไม่มีข้อความ
copied message info in history
+
+ not synchronized
+ No comment provided by engineer.
+
observer
ผู้สังเกตการณ์
@@ -8848,6 +8973,7 @@ pref value
ปิด
enabled status
group pref value
+member criteria value
time to disappear
@@ -8895,6 +9021,10 @@ time to disappear
pending approval
No comment provided by engineer.
+
+ pending review
+ No comment provided by engineer.
+
quantum resistant e2e encryption
chat item text
@@ -8932,6 +9062,10 @@ time to disappear
removed contact address
profile update event chat item
+
+ removed from group
+ No comment provided by engineer.
+
removed profile picture
profile update event chat item
@@ -8941,10 +9075,22 @@ time to disappear
ลบคุณออกแล้ว
rcv group event chat item
+
+ request to join rejected
+ No comment provided by engineer.
+
requested to connect
chat list item title
+
+ review
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ No comment provided by engineer.
+
saved
No comment provided by engineer.
@@ -9115,6 +9261,10 @@ last received msg: %2$@
you
No comment provided by engineer.
+
+ you accepted this member
+ snd group event chat item
+
you are invited to group
คุณได้รับเชิญให้เข้าร่วมกลุ่ม
diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff
index bbee40c2b9..d17a272016 100644
--- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff
+++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff
@@ -563,6 +563,14 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ alert action
+
+
+ Accept as observer
+ alert action
+
Accept conditions
Koşulları kabul et
@@ -584,6 +592,10 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ alert title
+
Accepted conditions
Kabul edilmiş koşullar
@@ -1557,11 +1569,23 @@ set passcode view
Sohbet senden silinecek - bu geri alınamaz!
No comment provided by engineer.
+
+ Chat with admins
+ chat toolbar
+
+
+ Chat with member
+ No comment provided by engineer.
+
Chats
Sohbetler
No comment provided by engineer.
+
+ Chats with members
+ No comment provided by engineer.
+
Check messages every 20 min.
Her 20 dakikada mesajları kontrol et.
@@ -2364,6 +2388,10 @@ swipe action
Sohbet profili silinsin mi?
No comment provided by engineer.
+
+ Delete chat with member?
+ alert title
+
Delete chat?
Sohbet silinsin mi?
@@ -2780,7 +2808,7 @@ swipe action
Don't show again
Yeniden gösterme
- No comment provided by engineer.
+ alert action
Done
@@ -3095,6 +3123,10 @@ chat item action
Bağlantı isteği kabul edilirken hata oluştu
No comment provided by engineer.
+
+ Error accepting member
+ alert title
+
Error adding member(s)
Üye(ler) eklenirken hata oluştu
@@ -3187,6 +3219,10 @@ chat item action
Sohbet veritabanı silinirken sorun oluştu
No comment provided by engineer.
+
+ Error deleting chat with member
+ alert title
+
Error deleting chat!
Sohbet silinirken hata oluştu!
@@ -3294,7 +3330,7 @@ chat item action
Error removing member
Kişiyi silerken sorun oluştu
- No comment provided by engineer.
+ alert title
Error reordering lists
@@ -4653,6 +4689,10 @@ Bu senin grup için bağlantın %@!
Kişi
No comment provided by engineer.
+
+ Member admission
+ No comment provided by engineer.
+
Member inactive
Üye inaktif
@@ -4686,6 +4726,10 @@ Bu senin grup için bağlantın %@!
Üye gruptan çıkarılacaktır - bu geri alınamaz!
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ alert message
+
Members can add message reactions.
Grup üyeleri mesaj tepkileri ekleyebilir.
@@ -5096,6 +5140,10 @@ Bu senin grup için bağlantın %@!
Yeni üye rolü
No comment provided by engineer.
+
+ New member wants to join the group.
+ rcv group event chat item
+
New message
Yeni mesaj
@@ -5132,6 +5180,10 @@ Bu senin grup için bağlantın %@!
No chats in list %@
No comment provided by engineer.
+
+ No chats with members
+ No comment provided by engineer.
+
No contacts selected
Hiçbir kişi seçilmedi
@@ -5311,7 +5363,8 @@ Bu senin grup için bağlantın %@!
Ok
Tamam
- alert button
+ alert action
+alert button
Old database
@@ -5711,6 +5764,10 @@ Hata: %@
Please try to disable and re-enable notfications.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ snd group event chat item
+
Please wait for token activation to complete.
token info
@@ -6153,6 +6210,10 @@ swipe action
Bağlanma isteğini reddet
No comment provided by engineer.
+
+ Reject member?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
Yönlendirici sunucusu yalnızca gerekli olduğunda kullanılır. Başka bir taraf IP adresinizi gözlemleyebilir.
@@ -6258,6 +6319,10 @@ swipe action
Report reason?
No comment provided by engineer.
+
+ Report sent to moderators
+ alert title
+
Report spam: only group moderators will see it.
report reason
@@ -6367,6 +6432,14 @@ swipe action
Review conditions
No comment provided by engineer.
+
+ Review members
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ admission stage description
+
Revoke
İptal et
@@ -6423,6 +6496,10 @@ chat item action
Kaydet (ve kişilere bildir)
alert button
+
+ Save admission settings?
+ alert title
+
Save and notify contact
Kaydet ve kişilere bildir
@@ -6931,6 +7008,10 @@ chat item action
Sistem kimlik doğrulaması yerine ayarla.
No comment provided by engineer.
+
+ Set member admission
+ No comment provided by engineer.
+
Set message expiration in chats.
No comment provided by engineer.
@@ -8646,6 +8727,10 @@ Katılma isteği tekrarlansın mı?
Bağlantı detaylarından davet bağlantısını yeniden görüntüleyebilirsin.
alert message
+
+ You can view your reports in Chat with admins.
+ alert message
+
You can't send messages!
Mesajlar gönderemezsiniz!
@@ -8945,6 +9030,10 @@ Bağlantı isteği tekrarlansın mı?
yukarı çıkın, ardından seçin:
No comment provided by engineer.
+
+ accepted %@
+ rcv group event chat item
+
accepted call
kabul edilen arama
@@ -8954,6 +9043,10 @@ Bağlantı isteği tekrarlansın mı?
accepted invitation
chat list item title
+
+ accepted you
+ rcv group event chat item
+
admin
yönetici
@@ -8974,6 +9067,10 @@ Bağlantı isteği tekrarlansın mı?
şifreleme kabul ediliyor…
chat item text
+
+ all
+ member criteria value
+
all members
bütün üyeler
@@ -9059,6 +9156,10 @@ marked deleted chat item preview text
aranıyor…
call status
+
+ can't send messages
+ No comment provided by engineer.
+
cancelled %@
%@ iptal edildi
@@ -9164,6 +9265,14 @@ marked deleted chat item preview text
%1$@ kişisi %2$@ olarak değişti
profile update event chat item
+
+ contact deleted
+ No comment provided by engineer.
+
+
+ contact disabled
+ No comment provided by engineer.
+
contact has e2e encryption
kişi uçtan uca şifrelemeye sahiptir
@@ -9174,6 +9283,10 @@ marked deleted chat item preview text
kişi uçtan uca şifrelemeye sahip değildir
No comment provided by engineer.
+
+ contact not ready
+ No comment provided by engineer.
+
creator
oluşturan
@@ -9345,6 +9458,10 @@ pref value
grup silindi
No comment provided by engineer.
+
+ group is deleted
+ No comment provided by engineer.
+
group profile updated
grup profili güncellendi
@@ -9470,6 +9587,10 @@ pref value
bağlanıldı
rcv group event chat item
+
+ member has old version
+ No comment provided by engineer.
+
message
mesaj
@@ -9534,6 +9655,10 @@ pref value
metin yok
copied message info in history
+
+ not synchronized
+ No comment provided by engineer.
+
observer
gözlemci
@@ -9544,6 +9669,7 @@ pref value
kapalı
enabled status
group pref value
+member criteria value
time to disappear
@@ -9594,6 +9720,10 @@ time to disappear
pending approval
No comment provided by engineer.
+
+ pending review
+ No comment provided by engineer.
+
quantum resistant e2e encryption
kuantuma dayanıklı e2e şifreleme
@@ -9633,6 +9763,10 @@ time to disappear
kişi adresi silindi
profile update event chat item
+
+ removed from group
+ No comment provided by engineer.
+
removed profile picture
profil fotoğrafı silindi
@@ -9643,10 +9777,22 @@ time to disappear
sen kaldırıldın
rcv group event chat item
+
+ request to join rejected
+ No comment provided by engineer.
+
requested to connect
chat list item title
+
+ review
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ No comment provided by engineer.
+
saved
kaydedildi
@@ -9836,6 +9982,10 @@ son alınan msj: %2$@
sen
No comment provided by engineer.
+
+ you accepted this member
+ snd group event chat item
+
you are invited to group
gruba davet edildiniz
diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff
index c0375e3b02..687393cfab 100644
--- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff
+++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff
@@ -563,6 +563,14 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ alert action
+
+
+ Accept as observer
+ alert action
+
Accept conditions
Прийняти умови
@@ -584,6 +592,10 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ alert title
+
Accepted conditions
Прийняті умови
@@ -1557,11 +1569,23 @@ set passcode view
Чат буде видалено для вас - цю дію неможливо скасувати!
No comment provided by engineer.
+
+ Chat with admins
+ chat toolbar
+
+
+ Chat with member
+ No comment provided by engineer.
+
Chats
Чати
No comment provided by engineer.
+
+ Chats with members
+ No comment provided by engineer.
+
Check messages every 20 min.
Перевіряйте повідомлення кожні 20 хв.
@@ -2364,6 +2388,10 @@ swipe action
Видалити профіль чату?
No comment provided by engineer.
+
+ Delete chat with member?
+ alert title
+
Delete chat?
Видалити чат?
@@ -2781,7 +2809,7 @@ swipe action
Don't show again
Більше не показувати
- No comment provided by engineer.
+ alert action
Done
@@ -3096,6 +3124,10 @@ chat item action
Помилка при прийнятті запиту на контакт
No comment provided by engineer.
+
+ Error accepting member
+ alert title
+
Error adding member(s)
Помилка додавання користувача(ів)
@@ -3188,6 +3220,10 @@ chat item action
Помилка видалення бази даних чату
No comment provided by engineer.
+
+ Error deleting chat with member
+ alert title
+
Error deleting chat!
Помилка видалення чату!
@@ -3295,7 +3331,7 @@ chat item action
Error removing member
Помилка видалення учасника
- No comment provided by engineer.
+ alert title
Error reordering lists
@@ -4654,6 +4690,10 @@ This is your link for group %@!
Учасник
No comment provided by engineer.
+
+ Member admission
+ No comment provided by engineer.
+
Member inactive
Користувач неактивний
@@ -4688,6 +4728,10 @@ This is your link for group %@!
Учасник буде видалений з групи - це неможливо скасувати!
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ alert message
+
Members can add message reactions.
Учасники групи можуть додавати реакції на повідомлення.
@@ -5102,6 +5146,10 @@ This is your link for group %@!
Нова роль учасника
No comment provided by engineer.
+
+ New member wants to join the group.
+ rcv group event chat item
+
New message
Нове повідомлення
@@ -5139,6 +5187,10 @@ This is your link for group %@!
No chats in list %@
No comment provided by engineer.
+
+ No chats with members
+ No comment provided by engineer.
+
No contacts selected
Не вибрано жодного контакту
@@ -5325,7 +5377,8 @@ This is your link for group %@!
Ok
Гаразд
- alert button
+ alert action
+alert button
Old database
@@ -5732,6 +5785,10 @@ Error: %@
Please try to disable and re-enable notfications.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ snd group event chat item
+
Please wait for token activation to complete.
token info
@@ -6176,6 +6233,10 @@ swipe action
Відхилити запит на контакт
No comment provided by engineer.
+
+ Reject member?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
Релейний сервер використовується тільки в разі потреби. Інша сторона може бачити вашу IP-адресу.
@@ -6281,6 +6342,10 @@ swipe action
Report reason?
No comment provided by engineer.
+
+ Report sent to moderators
+ alert title
+
Report spam: only group moderators will see it.
report reason
@@ -6391,6 +6456,14 @@ swipe action
Умови перегляду
No comment provided by engineer.
+
+ Review members
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ admission stage description
+
Revoke
Відкликати
@@ -6447,6 +6520,10 @@ chat item action
Зберегти (і повідомити контактам)
alert button
+
+ Save admission settings?
+ alert title
+
Save and notify contact
Зберегти та повідомити контакт
@@ -6959,6 +7036,10 @@ chat item action
Встановіть його замість аутентифікації системи.
No comment provided by engineer.
+
+ Set member admission
+ No comment provided by engineer.
+
Set message expiration in chats.
No comment provided by engineer.
@@ -8704,6 +8785,10 @@ Repeat join request?
Ви можете переглянути посилання на запрошення ще раз у деталях підключення.
alert message
+
+ You can view your reports in Chat with admins.
+ alert message
+
You can't send messages!
Ви не можете надсилати повідомлення!
@@ -9005,6 +9090,10 @@ Repeat connection request?
вище, а потім обирайте:
No comment provided by engineer.
+
+ accepted %@
+ rcv group event chat item
+
accepted call
прийнято виклик
@@ -9015,6 +9104,10 @@ Repeat connection request?
прийняте запрошення
chat list item title
+
+ accepted you
+ rcv group event chat item
+
admin
адмін
@@ -9035,6 +9128,10 @@ Repeat connection request?
узгодження шифрування…
chat item text
+
+ all
+ member criteria value
+
all members
всі учасники
@@ -9120,6 +9217,10 @@ marked deleted chat item preview text
дзвоніть…
call status
+
+ can't send messages
+ No comment provided by engineer.
+
cancelled %@
скасовано %@
@@ -9225,6 +9326,14 @@ marked deleted chat item preview text
контакт %1$@ змінено на %2$@
profile update event chat item
+
+ contact deleted
+ No comment provided by engineer.
+
+
+ contact disabled
+ No comment provided by engineer.
+
contact has e2e encryption
контакт має шифрування e2e
@@ -9235,6 +9344,10 @@ marked deleted chat item preview text
контакт не має шифрування e2e
No comment provided by engineer.
+
+ contact not ready
+ No comment provided by engineer.
+
creator
творець
@@ -9406,6 +9519,10 @@ pref value
групу видалено
No comment provided by engineer.
+
+ group is deleted
+ No comment provided by engineer.
+
group profile updated
оновлено профіль групи
@@ -9531,6 +9648,10 @@ pref value
з'єднаний
rcv group event chat item
+
+ member has old version
+ No comment provided by engineer.
+
message
повідомлення
@@ -9595,6 +9716,10 @@ pref value
без тексту
copied message info in history
+
+ not synchronized
+ No comment provided by engineer.
+
observer
спостерігач
@@ -9605,6 +9730,7 @@ pref value
вимкнено
enabled status
group pref value
+member criteria value
time to disappear
@@ -9655,6 +9781,10 @@ time to disappear
pending approval
No comment provided by engineer.
+
+ pending review
+ No comment provided by engineer.
+
quantum resistant e2e encryption
квантово-стійке шифрування e2e
@@ -9694,6 +9824,10 @@ time to disappear
видалено контактну адресу
profile update event chat item
+
+ removed from group
+ No comment provided by engineer.
+
removed profile picture
видалено зображення профілю
@@ -9704,11 +9838,23 @@ time to disappear
прибрали вас
rcv group event chat item
+
+ request to join rejected
+ No comment provided by engineer.
+
requested to connect
запит на підключення
chat list item title
+
+ review
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ No comment provided by engineer.
+
saved
збережено
@@ -9898,6 +10044,10 @@ last received msg: %2$@
ти
No comment provided by engineer.
+
+ you accepted this member
+ snd group event chat item
+
you are invited to group
вас запрошують до групи
diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff
index d5411f86e3..06ce8d4950 100644
--- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff
+++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff
@@ -565,6 +565,14 @@ time interval
accept incoming call via notification
swipe action
+
+ Accept as member
+ alert action
+
+
+ Accept as observer
+ alert action
+
Accept conditions
接受条款
@@ -586,6 +594,10 @@ swipe action
accept contact request via notification
swipe action
+
+ Accept member
+ alert title
+
Accepted conditions
已接受的条款
@@ -1582,11 +1594,23 @@ set passcode view
将为你删除聊天 - 此操作无法撤销!
No comment provided by engineer.
+
+ Chat with admins
+ chat toolbar
+
+
+ Chat with member
+ No comment provided by engineer.
+
Chats
聊天
No comment provided by engineer.
+
+ Chats with members
+ No comment provided by engineer.
+
Check messages every 20 min.
每 20 分钟检查消息。
@@ -2401,6 +2425,10 @@ swipe action
删除聊天资料?
No comment provided by engineer.
+
+ Delete chat with member?
+ alert title
+
Delete chat?
删除聊天?
@@ -2824,7 +2852,7 @@ swipe action
Don't show again
不再显示
- No comment provided by engineer.
+ alert action
Done
@@ -3142,6 +3170,10 @@ chat item action
接受联系人请求错误
No comment provided by engineer.
+
+ Error accepting member
+ alert title
+
Error adding member(s)
添加成员错误
@@ -3236,6 +3268,10 @@ chat item action
删除聊天数据库错误
No comment provided by engineer.
+
+ Error deleting chat with member
+ alert title
+
Error deleting chat!
删除聊天错误!
@@ -3344,7 +3380,7 @@ chat item action
Error removing member
删除成员错误
- No comment provided by engineer.
+ alert title
Error reordering lists
@@ -4728,6 +4764,10 @@ This is your link for group %@!
成员
No comment provided by engineer.
+
+ Member admission
+ No comment provided by engineer.
+
Member inactive
成员不活跃
@@ -4763,6 +4803,10 @@ This is your link for group %@!
成员将被移出群组——此操作无法撤消!
No comment provided by engineer.
+
+ Member will join the group, accept member?
+ alert message
+
Members can add message reactions.
群组成员可以添加信息回应。
@@ -5183,6 +5227,10 @@ This is your link for group %@!
新成员角色
No comment provided by engineer.
+
+ New member wants to join the group.
+ rcv group event chat item
+
New message
新消息
@@ -5223,6 +5271,10 @@ This is your link for group %@!
列表 %@ 中无聊天
No comment provided by engineer.
+
+ No chats with members
+ No comment provided by engineer.
+
No contacts selected
未选择联系人
@@ -5415,7 +5467,8 @@ This is your link for group %@!
Ok
好的
- alert button
+ alert action
+alert button
Old database
@@ -5823,6 +5876,10 @@ Error: %@
Please try to disable and re-enable notfications.
token info
+
+ Please wait for group moderators to review your request to join the group.
+ snd group event chat item
+
Please wait for token activation to complete.
token info
@@ -6265,6 +6322,10 @@ swipe action
拒绝联系人请求
No comment provided by engineer.
+
+ Reject member?
+ alert title
+
Relay server is only used if necessary. Another party can observe your IP address.
中继服务器仅在必要时使用。其他人可能会观察到您的IP地址。
@@ -6369,6 +6430,10 @@ swipe action
Report reason?
No comment provided by engineer.
+
+ Report sent to moderators
+ alert title
+
Report spam: only group moderators will see it.
report reason
@@ -6479,6 +6544,14 @@ swipe action
审阅条款
No comment provided by engineer.
+
+ Review members
+ admission stage
+
+
+ Review members before admitting ("knocking").
+ admission stage description
+
Revoke
吊销
@@ -6534,6 +6607,10 @@ chat item action
保存(并通知联系人)
alert button
+
+ Save admission settings?
+ alert title
+
Save and notify contact
保存并通知联系人
@@ -7041,6 +7118,10 @@ chat item action
设置它以代替系统身份验证。
No comment provided by engineer.
+
+ Set member admission
+ No comment provided by engineer.
+
Set message expiration in chats.
No comment provided by engineer.
@@ -8750,6 +8831,10 @@ Repeat join request?
您可以在连接详情中再次查看邀请链接。
alert message
+
+ You can view your reports in Chat with admins.
+ alert message
+
You can't send messages!
您无法发送消息!
@@ -9045,6 +9130,10 @@ Repeat connection request?
上面,然后选择:
No comment provided by engineer.
+
+ accepted %@
+ rcv group event chat item
+
accepted call
已接受通话
@@ -9054,6 +9143,10 @@ Repeat connection request?
accepted invitation
chat list item title
+
+ accepted you
+ rcv group event chat item
+
admin
管理员
@@ -9074,6 +9167,10 @@ Repeat connection request?
同意加密…
chat item text
+
+ all
+ member criteria value
+
all members
所有成员
@@ -9159,6 +9256,10 @@ marked deleted chat item preview text
呼叫中……
call status
+
+ can't send messages
+ No comment provided by engineer.
+
cancelled %@
已取消 %@
@@ -9264,6 +9365,14 @@ marked deleted chat item preview text
联系人 %1$@ 已更改为 %2$@
profile update event chat item
+
+ contact deleted
+ No comment provided by engineer.
+
+
+ contact disabled
+ No comment provided by engineer.
+
contact has e2e encryption
联系人具有端到端加密
@@ -9274,6 +9383,10 @@ marked deleted chat item preview text
联系人没有端到端加密
No comment provided by engineer.
+
+ contact not ready
+ No comment provided by engineer.
+
creator
创建者
@@ -9445,6 +9558,10 @@ pref value
群组已删除
No comment provided by engineer.
+
+ group is deleted
+ No comment provided by engineer.
+
group profile updated
群组资料已更新
@@ -9570,6 +9687,10 @@ pref value
已连接
rcv group event chat item
+
+ member has old version
+ No comment provided by engineer.
+
message
消息
@@ -9634,6 +9755,10 @@ pref value
无文本
copied message info in history
+
+ not synchronized
+ No comment provided by engineer.
+
observer
观察者
@@ -9644,6 +9769,7 @@ pref value
关闭
enabled status
group pref value
+member criteria value
time to disappear
@@ -9694,6 +9820,10 @@ time to disappear
pending approval
No comment provided by engineer.
+
+ pending review
+ No comment provided by engineer.
+
quantum resistant e2e encryption
抗量子端到端加密
@@ -9733,6 +9863,10 @@ time to disappear
删除了联系地址
profile update event chat item
+
+ removed from group
+ No comment provided by engineer.
+
removed profile picture
删除了资料图片
@@ -9743,10 +9877,22 @@ time to disappear
已将您移除
rcv group event chat item
+
+ request to join rejected
+ No comment provided by engineer.
+
requested to connect
chat list item title
+
+ review
+ No comment provided by engineer.
+
+
+ reviewed by admins
+ No comment provided by engineer.
+
saved
已保存
@@ -9936,6 +10082,10 @@ last received msg: %2$@
您
No comment provided by engineer.
+
+ you accepted this member
+ snd group event chat item
+
you are invited to group
您被邀请加入群组
diff --git a/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings
index 7205b37e7f..cf082a166d 100644
--- a/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings
+++ b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings
@@ -1,6 +1,9 @@
/* notification body */
"%d new events" = "%d новых сообщений";
+/* notification body */
+"From %d chat(s)" = "Из %d чатов";
+
/* notification body */
"From: %@" = "От: %@";
diff --git a/apps/ios/SimpleX SE/ShareAPI.swift b/apps/ios/SimpleX SE/ShareAPI.swift
index 3e901c73eb..2b3e8068ae 100644
--- a/apps/ios/SimpleX SE/ShareAPI.swift
+++ b/apps/ios/SimpleX SE/ShareAPI.swift
@@ -67,6 +67,7 @@ func apiSendMessages(
: SEChatCommand.apiSendMessages(
type: chatInfo.chatType,
id: chatInfo.apiId,
+ scope: chatInfo.groupChatScope(),
live: false,
ttl: nil,
composedMessages: composedMessages
@@ -123,7 +124,7 @@ enum SEChatCommand: ChatCmdProtocol {
case apiSetEncryptLocalFiles(enable: Bool)
case apiGetChats(userId: Int64)
case apiCreateChatItems(noteFolderId: Int64, composedMessages: [ComposedMessage])
- case apiSendMessages(type: ChatType, id: Int64, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
+ case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
var cmdString: String {
switch self {
@@ -139,15 +140,27 @@ enum SEChatCommand: ChatCmdProtocol {
case let .apiCreateChatItems(noteFolderId, composedMessages):
let msgs = encodeJSON(composedMessages)
return "/_create *\(noteFolderId) json \(msgs)"
- case let .apiSendMessages(type, id, live, ttl, composedMessages):
+ case let .apiSendMessages(type, id, scope, live, ttl, composedMessages):
let msgs = encodeJSON(composedMessages)
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
- return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
+ return "/_send \(ref(type, id, scope: scope)) live=\(onOff(live)) ttl=\(ttlStr) json \(msgs)"
}
}
- func ref(_ type: ChatType, _ id: Int64) -> String {
- "\(type.rawValue)\(id)"
+ func ref(_ type: ChatType, _ id: Int64, scope: GroupChatScope?) -> String {
+ "\(type.rawValue)\(id)\(scopeRef(scope: scope))"
+ }
+
+ func scopeRef(scope: GroupChatScope?) -> String {
+ switch (scope) {
+ case .none: ""
+ case let .memberSupport(groupMemberId_):
+ if let groupMemberId = groupMemberId_ {
+ "(_support:\(groupMemberId))"
+ } else {
+ "(_support)"
+ }
+ }
}
}
diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj
index 9326ae9abe..42e36a78c9 100644
--- a/apps/ios/SimpleX.xcodeproj/project.pbxproj
+++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj
@@ -168,14 +168,19 @@
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.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 */; };
+ 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a */; };
+ 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.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 */; };
@@ -527,14 +532,19 @@
6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = ""; };
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = ""; };
+ 64A779F52DBFB9F200FDEF2F /* MemberAdmissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberAdmissionView.swift; sourceTree = ""; };
+ 64A779F72DBFDBF200FDEF2F /* MemberSupportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberSupportView.swift; sourceTree = ""; };
+ 64A779FB2DC1040000FDEF2F /* SecondaryChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryChatView.swift; sourceTree = ""; };
+ 64A779FD2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberSupportChatToolbar.swift; sourceTree = ""; };
+ 64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextPendingMemberActionsView.swift; sourceTree = ""; };
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = ""; };
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = ""; };
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = ""; };
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; };
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; };
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; };
- 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 = ""; };
- 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a"; sourceTree = ""; };
+ 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a"; sourceTree = ""; };
+ 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a"; sourceTree = ""; };
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; };
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; };
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; };
@@ -692,8 +702,8 @@
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
64C829A12D54AEEE006B9E89 /* libgmpxx.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 */,
+ 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a in Frameworks */,
+ 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -778,8 +788,8 @@
64C829992D54AEEE006B9E89 /* libffi.a */,
64C829982D54AEED006B9E89 /* libgmp.a */,
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
- 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss-ghc9.6.3.a */,
- 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.6.0-64eNxtIoLF9BaOhAoPagss.a */,
+ 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a */,
+ 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a */,
);
path = Libraries;
sourceTree = "";
@@ -1076,6 +1086,7 @@
644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */,
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */,
6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */,
+ 64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */,
);
path = ComposeMessage;
sourceTree = "";
@@ -1117,6 +1128,10 @@
6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */,
1841516F0CE5992B0EDFB377 /* GroupWelcomeView.swift */,
B70CE9E52D4BE5930080F36D /* GroupMentions.swift */,
+ 64A779F52DBFB9F200FDEF2F /* MemberAdmissionView.swift */,
+ 64A779F72DBFDBF200FDEF2F /* MemberSupportView.swift */,
+ 64A779FB2DC1040000FDEF2F /* SecondaryChatView.swift */,
+ 64A779FD2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift */,
);
path = Group;
sourceTree = "";
@@ -1431,8 +1446,10 @@
640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */,
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */,
640743612CD360E600158442 /* ChooseServerOperators.swift in Sources */,
+ 64A779FE2DC3AFF200FDEF2F /* MemberSupportChatToolbar.swift in Sources */,
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */,
5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */,
+ 64A779F82DBFDBF200FDEF2F /* MemberSupportView.swift in Sources */,
5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */,
5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */,
B79ADAFF2CE4EF930083DFFD /* AddressCreationCard.swift in Sources */,
@@ -1467,6 +1484,7 @@
5CB634AF29E4BB7D0066AD6B /* SetAppPasscodeView.swift in Sources */,
5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */,
5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */,
+ 64A779FC2DC1040000FDEF2F /* SecondaryChatView.swift in Sources */,
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */,
648679AB2BC96A74006456E7 /* ChatItemForwardingView.swift in Sources */,
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */,
@@ -1509,6 +1527,7 @@
5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */,
647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */,
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */,
+ 64A77A022DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift in Sources */,
8C74C3EA2C1B90AF00039E77 /* ThemeManager.swift in Sources */,
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */,
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */,
@@ -1587,6 +1606,7 @@
1841560FD1CD447955474C1D /* UserProfilesView.swift in Sources */,
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */,
8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */,
+ 64A779F62DBFB9F200FDEF2F /* MemberAdmissionView.swift in Sources */,
18415C6C56DBCEC2CBBD2F11 /* WebRTCClient.swift in Sources */,
8CB15EA02CFDA30600C28209 /* ChatItemsMerger.swift in Sources */,
184152CEF68D2336FC2EBCB0 /* CallViewRenderers.swift in Sources */,
@@ -1971,7 +1991,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 282;
+ CURRENT_PROJECT_VERSION = 279;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1996,7 +2016,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES_THIN;
- MARKETING_VERSION = 6.3.6;
+ MARKETING_VERSION = 6.4;
OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000";
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
@@ -2021,7 +2041,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 282;
+ CURRENT_PROJECT_VERSION = 279;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -2046,7 +2066,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 6.3.6;
+ MARKETING_VERSION = 6.4;
OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000";
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
@@ -2063,11 +2083,11 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 282;
+ CURRENT_PROJECT_VERSION = 279;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
- MARKETING_VERSION = 6.3.6;
+ MARKETING_VERSION = 6.4;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2083,11 +2103,11 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 282;
+ CURRENT_PROJECT_VERSION = 279;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
- MARKETING_VERSION = 6.3.6;
+ MARKETING_VERSION = 6.4;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2108,7 +2128,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 282;
+ CURRENT_PROJECT_VERSION = 279;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -2123,7 +2143,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 6.3.6;
+ MARKETING_VERSION = 6.4;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2145,7 +2165,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 282;
+ CURRENT_PROJECT_VERSION = 279;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -2160,7 +2180,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 6.3.6;
+ MARKETING_VERSION = 6.4;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2182,7 +2202,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 282;
+ CURRENT_PROJECT_VERSION = 279;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2208,7 +2228,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 6.3.6;
+ MARKETING_VERSION = 6.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -2233,7 +2253,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 282;
+ CURRENT_PROJECT_VERSION = 279;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2259,7 +2279,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
- MARKETING_VERSION = 6.3.6;
+ MARKETING_VERSION = 6.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -2284,7 +2304,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 282;
+ CURRENT_PROJECT_VERSION = 279;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2299,7 +2319,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MARKETING_VERSION = 6.3.6;
+ MARKETING_VERSION = 6.4;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2318,7 +2338,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 282;
+ CURRENT_PROJECT_VERSION = 279;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2333,7 +2353,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
- MARKETING_VERSION = 6.3.6;
+ MARKETING_VERSION = 6.4;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift
index 88246465e1..9a82c912dd 100644
--- a/apps/ios/SimpleXChat/ChatTypes.swift
+++ b/apps/ios/SimpleXChat/ChatTypes.swift
@@ -15,6 +15,9 @@ public let CREATE_MEMBER_CONTACT_VERSION = 2
// version to receive reports (MCReport)
public let REPORTS_VERSION = 12
+// support group knocking (MsgScope)
+public let GROUP_KNOCKING_VERSION = 15
+
public let contentModerationPostLink = URL(string: "https://simplex.chat/blog/20250114-simplex-network-large-groups-privacy-preserving-content-moderation.html#preventing-server-abuse-without-compromising-e2e-encryption")!
public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable {
@@ -1197,7 +1200,7 @@ public enum GroupFeatureEnabled: String, Codable, Identifiable, Hashable {
public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
case direct(contact: Contact)
- case group(groupInfo: GroupInfo)
+ case group(groupInfo: GroupInfo, groupChatScope: GroupChatScopeInfo?)
case local(noteFolder: NoteFolder)
case contactRequest(contactRequest: UserContactRequest)
case contactConnection(contactConnection: PendingContactConnection)
@@ -1211,7 +1214,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 +1227,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 +1240,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 +1253,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 +1266,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 +1279,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 +1305,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 +1318,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
@@ -1336,34 +1339,57 @@ 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)
+ case let .direct(contact):
+ // TODO [short links] this will have additional statuses for pending contact requests before they are accepted
+ if contact.nextSendGrpInv { return nil }
+ if !contact.active { return ("contact deleted", nil) }
+ if !contact.sndReady { return ("contact not ready", nil) }
+ if contact.activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false { return ("not synchronized", nil) }
+ if contact.activeConn?.connDisabled ?? true { return ("contact disabled", nil) }
+ return nil
+ case let .group(groupInfo, groupChatScope):
+ if groupInfo.membership.memberActive {
+ switch(groupChatScope) {
+ case .none:
+ if groupInfo.membership.memberPending { return ("reviewed by admins", "Please contact group admin.") }
+ if groupInfo.membership.memberRole == .observer { return ("you are observer", "Please contact group admin.") }
+ return nil
+ case let .some(.memberSupport(groupMember_: .some(supportMember))):
+ if supportMember.versionRange.maxVersion < GROUP_KNOCKING_VERSION && !supportMember.memberPending {
+ return ("member has old version", nil)
+ }
+ return nil
+ case .some(.memberSupport(groupMember_: .none)):
+ return nil
+ }
+ } else {
+ switch groupInfo.membership.memberStatus {
+ case .memRejected: return ("request to join rejected", nil)
+ case .memGroupDeleted: return ("group is deleted", nil)
+ case .memRemoved: return ("removed from group", nil)
+ case .memLeft: return ("you left", nil)
+ default: return ("can't send messages", nil)
+ }
+ }
+ case .local:
+ return nil
+ case .contactRequest:
+ return ("can't send messages", nil)
+ case .contactConnection:
+ return ("can't send messages", nil)
+ 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 .local(noteFolder): return noteFolder.sendMsgEnabled
- case let .contactRequest(contactRequest): return contactRequest.sendMsgEnabled
- case let .contactConnection(contactConnection): return contactConnection.sendMsgEnabled
- case .invalidJSON: return false
- }
- }
- }
+ public var sendMsgEnabled: Bool { userCantSendReason == nil }
public var incognito: Bool {
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
@@ -1388,7 +1414,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
}
}
@@ -1405,7 +1431,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
@@ -1428,7 +1454,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:
@@ -1453,7 +1479,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 {
@@ -1484,7 +1510,14 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
return .other
}
}
-
+
+ public func groupChatScope() -> GroupChatScope? {
+ switch self {
+ case let .group(_, groupChatScope): groupChatScope?.toChatScope()
+ default: nil
+ }
+ }
+
public func ntfsEnabled(chatItem: ChatItem) -> Bool {
ntfsEnabled(chatItem.meta.userMention)
}
@@ -1500,7 +1533,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
}
}
@@ -1516,7 +1549,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
}
}
@@ -1524,7 +1557,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
@@ -1535,7 +1568,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
@@ -1546,7 +1579,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
@@ -1562,7 +1595,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 {
@@ -1582,7 +1615,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
public static var sampleData: ChatInfo.SampleData = SampleData(
direct: ChatInfo.direct(contact: Contact.sampleData),
- group: ChatInfo.group(groupInfo: GroupInfo.sampleData),
+ group: ChatInfo.group(groupInfo: GroupInfo.sampleData, groupChatScope: nil),
local: ChatInfo.local(noteFolder: NoteFolder.sampleData),
contactRequest: ChatInfo.contactRequest(contactRequest: UserContactRequest.sampleData),
contactConnection: ChatInfo.contactConnection(contactConnection: PendingContactConnection.getSampleData())
@@ -1612,7 +1645,13 @@ 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,6 +1668,32 @@ 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
@@ -1655,16 +1720,6 @@ 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 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 } }
@@ -1843,8 +1898,6 @@ 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 } }
public var image: String? { get { profile.image } }
@@ -1876,8 +1929,6 @@ 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) }
}
@@ -2002,24 +2053,11 @@ 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 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 } }
@@ -2032,13 +2070,17 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
}
public var canDelete: Bool {
- return membership.memberRole == .owner || !membership.memberCurrent
+ return membership.memberRole == .owner || !membership.memberCurrentOrPending
}
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",
@@ -2048,6 +2090,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
chatSettings: ChatSettings.defaults,
createdAt: .now,
updatedAt: .now,
+ membersRequireAttention: 0,
chatTags: [],
localAlias: ""
)
@@ -2059,12 +2102,20 @@ public struct GroupRef: Decodable, Hashable {
}
public struct GroupProfile: Codable, NamedChat, Hashable {
- public init(displayName: String, fullName: String, description: String? = nil, image: String? = nil, groupPreferences: GroupPreferences? = nil) {
+ public init(
+ displayName: String,
+ fullName: String,
+ description: String? = nil,
+ image: String? = nil,
+ groupPreferences: GroupPreferences? = nil,
+ memberAdmission: GroupMemberAdmission? = nil
+ ) {
self.displayName = displayName
self.fullName = fullName
self.description = description
self.image = image
self.groupPreferences = groupPreferences
+ self.memberAdmission = memberAdmission
}
public var displayName: String
@@ -2072,14 +2123,48 @@ 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
@@ -2106,6 +2191,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
public var memberContactId: Int64?
public var memberContactProfileId: Int64
public var activeConn: Connection?
+ public var supportChat: GroupSupportChat?
public var memberChatVRange: VersionRange
public var id: String { "#\(groupId) @\(groupMemberId)" }
@@ -2177,6 +2263,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
case .memUnknown: return false
case .memInvited: return false
case .memPendingApproval: return true
+ case .memPendingReview: return true
case .memIntroduced: return false
case .memIntroInvited: return false
case .memAccepted: return false
@@ -2196,6 +2283,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
case .memUnknown: return false
case .memInvited: return false
case .memPendingApproval: return false
+ case .memPendingReview: return false
case .memIntroduced: return true
case .memIntroInvited: return true
case .memAccepted: return true
@@ -2206,6 +2294,18 @@ 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
@@ -2259,6 +2359,13 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
)
}
+public struct GroupSupportChat: Codable, Hashable {
+ public var chatTs: Date
+ public var unread: Int
+ public var memberAttention: Int
+ public var mentions: Int
+}
+
public struct GroupMemberSettings: Codable, Hashable {
public var showMessages: Bool
}
@@ -2328,6 +2435,7 @@ public enum GroupMemberStatus: String, Decodable, Hashable {
case memUnknown = "unknown"
case memInvited = "invited"
case memPendingApproval = "pending_approval"
+ case memPendingReview = "pending_review"
case memIntroduced = "introduced"
case memIntroInvited = "intro-inv"
case memAccepted = "accepted"
@@ -2345,6 +2453,7 @@ public enum GroupMemberStatus: String, Decodable, Hashable {
case .memUnknown: return "unknown status"
case .memInvited: return "invited"
case .memPendingApproval: return "pending approval"
+ case .memPendingReview: return "pending review"
case .memIntroduced: return "connecting (introduced)"
case .memIntroInvited: return "connecting (introduction invitation)"
case .memAccepted: return "connecting (accepted)"
@@ -2364,6 +2473,7 @@ public enum GroupMemberStatus: String, Decodable, Hashable {
case .memUnknown: return "unknown"
case .memInvited: return "invited"
case .memPendingApproval: return "pending"
+ case .memPendingReview: return "review"
case .memIntroduced: return "connecting"
case .memIntroInvited: return "connecting"
case .memAccepted: return "connecting"
@@ -2386,8 +2496,6 @@ 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 { "" } }
public var image: String? { get { nil } }
@@ -2647,12 +2755,15 @@ 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:
@@ -2685,6 +2796,8 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
switch rcvGroupEvent {
case .groupUpdated: return false
case .memberConnected: return false
+ case .memberAccepted: return false
+ case .userAccepted: return false
case .memberRole: return false
case .memberBlocked: return false
case .userRole: return true
@@ -2696,6 +2809,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
case .invitedViaGroupLink: return false
case .memberCreatedContact: return false
case .memberProfileUpdated: return false
+ case .newMemberPendingReview: return true
}
case .sndGroupEvent: return false
case .rcvConnEvent: return false
@@ -2764,12 +2878,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
@@ -3151,6 +3265,21 @@ public enum CIStatus: Decodable, Hashable {
}
}
+ // as in corresponds to SENT response from agent, opposed to `sent` which means snd status
+ public var isSent: Bool {
+ switch self {
+ case .sndNew: false
+ case .sndSent: true
+ case .sndRcvd: false
+ case .sndErrorAuth: true
+ case .sndError: true
+ case .sndWarning: true
+ case .rcvNew: false
+ case .rcvRead: false
+ case .invalid: false
+ }
+ }
+
public func statusIcon(_ metaColor: Color, _ paleMetaColor: Color, _ primaryColor: Color = .accentColor) -> (Image, Color)? {
switch self {
case .sndNew: nil
@@ -3204,6 +3333,17 @@ public enum CIStatus: Decodable, Hashable {
}
}
+public func shouldKeepOldSndCIStatus(oldStatus: CIStatus, newStatus: CIStatus) -> Bool {
+ switch (oldStatus, newStatus) {
+ case (.sndRcvd, let new) where !new.isSndRcvd:
+ return true
+ case (let old, .sndNew) where old.isSent:
+ return true
+ default:
+ return false
+ }
+}
+
public enum SndError: Decodable, Hashable {
case auth
case quota
@@ -4103,6 +4243,16 @@ extension MsgContent: Encodable {
}
}
+public enum MsgContentTag: String {
+ case text
+ case link
+ case image
+ case video
+ case voice
+ case file
+ case report
+}
+
public struct FormattedText: Decodable, Hashable {
public var text: String
public var format: Format?
@@ -4440,6 +4590,8 @@ public enum RcvDirectEvent: Decodable, Hashable {
public enum RcvGroupEvent: Decodable, Hashable {
case memberAdded(groupMemberId: Int64, profile: Profile)
case memberConnected
+ case memberAccepted(groupMemberId: Int64, profile: Profile)
+ case userAccepted
case memberLeft
case memberRole(groupMemberId: Int64, profile: Profile, role: GroupMemberRole)
case memberBlocked(groupMemberId: Int64, profile: Profile, blocked: Bool)
@@ -4451,12 +4603,16 @@ public enum RcvGroupEvent: Decodable, Hashable {
case invitedViaGroupLink
case memberCreatedContact
case memberProfileUpdated(fromProfile: Profile, toProfile: Profile)
+ case newMemberPendingReview
var text: String {
switch self {
case let .memberAdded(_, profile):
return String.localizedStringWithFormat(NSLocalizedString("invited %@", comment: "rcv group event chat item"), profile.profileViewName)
case .memberConnected: return NSLocalizedString("member connected", comment: "rcv group event chat item")
+ case let .memberAccepted(_, profile):
+ return String.localizedStringWithFormat(NSLocalizedString("accepted %@", comment: "rcv group event chat item"), profile.profileViewName)
+ case .userAccepted: return NSLocalizedString("accepted you", comment: "rcv group event chat item")
case .memberLeft: return NSLocalizedString("left", comment: "rcv group event chat item")
case let .memberRole(_, profile, role):
return String.localizedStringWithFormat(NSLocalizedString("changed role of %@ to %@", comment: "rcv group event chat item"), profile.profileViewName, role.text)
@@ -4476,6 +4632,7 @@ public enum RcvGroupEvent: Decodable, Hashable {
case .invitedViaGroupLink: return NSLocalizedString("invited via your group link", comment: "rcv group event chat item")
case .memberCreatedContact: return NSLocalizedString("connected directly", comment: "rcv group event chat item")
case let .memberProfileUpdated(fromProfile, toProfile): return profileUpdatedText(fromProfile, toProfile)
+ case .newMemberPendingReview: return NSLocalizedString("New member wants to join the group.", comment: "rcv group event chat item")
}
}
@@ -4500,6 +4657,8 @@ 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 {
@@ -4517,6 +4676,9 @@ 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")
}
}
}
diff --git a/apps/ios/SimpleXChat/ChatUtils.swift b/apps/ios/SimpleXChat/ChatUtils.swift
index 6cbc76ec98..6f80629932 100644
--- a/apps/ios/SimpleXChat/ChatUtils.swift
+++ b/apps/ios/SimpleXChat/ChatUtils.swift
@@ -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
@@ -82,9 +82,9 @@ 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 .local(noteFolder): noteFolder.sendMsgEnabled
+ case let .direct(contact): cInfo.sendMsgEnabled && !contact.nextSendGrpInv
+ case .group: cInfo.sendMsgEnabled
+ case .local: cInfo.sendMsgEnabled
case .contactRequest: false
case .contactConnection: false
case .invalidJSON: 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"
diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift
index 5579449caa..70db4476d5 100644
--- a/apps/ios/SimpleXChat/Notifications.swift
+++ b/apps/ios/SimpleXChat/Notifications.swift
@@ -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):"
diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings
index e4bc8f2150..1f4ff88f78 100644
--- a/apps/ios/bg.lproj/Localizable.strings
+++ b/apps/ios/bg.lproj/Localizable.strings
@@ -1485,7 +1485,7 @@ swipe action */
/* No comment provided by engineer. */
"Don't enable" = "Не активирай";
-/* No comment provided by engineer. */
+/* alert action */
"Don't show again" = "Не показвай отново";
/* No comment provided by engineer. */
@@ -1777,7 +1777,7 @@ chat item action */
/* alert title */
"Error receiving file" = "Грешка при получаване на файл";
-/* No comment provided by engineer. */
+/* alert title */
"Error removing member" = "Грешка при отстраняване на член";
/* No comment provided by engineer. */
@@ -2747,6 +2747,7 @@ snd error text */
/* enabled status
group pref value
+member criteria value
time to disappear */
"off" = "изключено";
@@ -2759,7 +2760,8 @@ time to disappear */
/* feature offered item */
"offered %@: %@" = "предлага %1$@: %2$@";
-/* alert button */
+/* alert action
+alert button */
"Ok" = "Ок";
/* No comment provided by engineer. */
@@ -3776,9 +3778,6 @@ chat item action */
/* No comment provided by engineer. */
"The old database was not removed during the migration, it can be deleted." = "Старата база данни не бе премахната по време на миграцията, тя може да бъде изтрита.";
-/* No comment provided by engineer. */
-"Your profile is stored on your device and only shared with your contacts." = "Профилът се споделя само с вашите контакти.";
-
/* No comment provided by engineer. */
"The second tick we missed! ✅" = "Втората отметка, която пропуснахме! ✅";
@@ -4446,10 +4445,10 @@ chat item action */
"Your profile **%@** will be shared." = "Вашият профил **%@** ще бъде споделен.";
/* No comment provided by engineer. */
-"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. SimpleX сървърите не могат да видят вашия профил.";
+"Your profile is stored on your device and only shared with your contacts." = "Профилът се споделя само с вашите контакти.";
/* No comment provided by engineer. */
-"Your profile, contacts and delivered messages are stored on your device." = "Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство.";
+"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. SimpleX сървърите не могат да видят вашия профил.";
/* No comment provided by engineer. */
"Your random profile" = "Вашият автоматично генериран профил";
diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings
index 08a94615a3..a3a6ca8215 100644
--- a/apps/ios/cs.lproj/Localizable.strings
+++ b/apps/ios/cs.lproj/Localizable.strings
@@ -1127,7 +1127,7 @@ swipe action */
/* No comment provided by engineer. */
"Don't enable" = "Nepovolovat";
-/* No comment provided by engineer. */
+/* alert action */
"Don't show again" = "Znovu neukazuj";
/* No comment provided by engineer. */
@@ -1367,7 +1367,7 @@ swipe action */
/* alert title */
"Error receiving file" = "Chyba při příjmu souboru";
-/* No comment provided by engineer. */
+/* alert title */
"Error removing member" = "Chyba při odebrání člena";
/* No comment provided by engineer. */
@@ -2145,6 +2145,7 @@ snd error text */
/* enabled status
group pref value
+member criteria value
time to disappear */
"off" = "vypnuto";
@@ -2157,7 +2158,8 @@ time to disappear */
/* feature offered item */
"offered %@: %@" = "nabídl %1$@: %2$@";
-/* alert button */
+/* alert action
+alert button */
"Ok" = "Ok";
/* No comment provided by engineer. */
@@ -2988,9 +2990,6 @@ chat item action */
/* No comment provided by engineer. */
"The old database was not removed during the migration, it can be deleted." = "Stará databáze nebyla během přenášení odstraněna, lze ji smazat.";
-/* No comment provided by engineer. */
-"Your profile is stored on your device and only shared with your contacts." = "Profil je sdílen pouze s vašimi kontakty.";
-
/* No comment provided by engineer. */
"The second tick we missed! ✅" = "Druhé zaškrtnutí jsme přehlédli! ✅";
@@ -3472,10 +3471,10 @@ chat item action */
"Your profile **%@** will be shared." = "Váš profil **%@** bude sdílen.";
/* No comment provided by engineer. */
-"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty. Servery SimpleX nevidí váš profil.";
+"Your profile is stored on your device and only shared with your contacts." = "Profil je sdílen pouze s vašimi kontakty.";
/* No comment provided by engineer. */
-"Your profile, contacts and delivered messages are stored on your device." = "Váš profil, kontakty a doručené zprávy jsou uloženy ve vašem zařízení.";
+"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty. Servery SimpleX nevidí váš profil.";
/* No comment provided by engineer. */
"Your random profile" = "Váš náhodný profil";
diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings
index 8da7835c43..4be4ad96ba 100644
--- a/apps/ios/de.lproj/Localizable.strings
+++ b/apps/ios/de.lproj/Localizable.strings
@@ -345,6 +345,12 @@ accept incoming call via notification
swipe action */
"Accept" = "Annehmen";
+/* alert action */
+"Accept as member" = "Als Mitglied übernehmen";
+
+/* alert action */
+"Accept as observer" = "Als Beobachter übernehmen";
+
/* No comment provided by engineer. */
"Accept conditions" = "Nutzungsbedingungen akzeptieren";
@@ -358,6 +364,12 @@ swipe action */
swipe action */
"Accept incognito" = "Inkognito akzeptieren";
+/* alert title */
+"Accept member" = "Mitglied übernehmen";
+
+/* rcv group event chat item */
+"accepted %@" = "%@ übernommen";
+
/* call status */
"accepted call" = "Anruf angenommen";
@@ -367,6 +379,9 @@ swipe action */
/* chat list item title */
"accepted invitation" = "Einladung angenommen";
+/* rcv group event chat item */
+"accepted you" = "hat Sie übernommen";
+
/* No comment provided by engineer. */
"Acknowledged" = "Bestätigt";
@@ -463,6 +478,9 @@ swipe action */
/* chat item text */
"agreeing encryption…" = "Verschlüsselung zustimmen…";
+/* member criteria value */
+"all" = "alle";
+
/* No comment provided by engineer. */
"All" = "Alle";
@@ -905,6 +923,9 @@ marked deleted chat item preview text */
/* No comment provided by engineer. */
"Can't message member" = "Mitglied kann nicht benachrichtigt werden";
+/* No comment provided by engineer. */
+"can't send messages" = "Es können keine Nachrichten gesendet werden";
+
/* alert action
alert button */
"Cancel" = "Abbrechen";
@@ -1042,9 +1063,18 @@ set passcode view */
/* No comment provided by engineer. */
"Chat will be deleted for you - this cannot be undone!" = "Der Chat wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!";
+/* chat toolbar */
+"Chat with admins" = "Chat mit Administratoren";
+
+/* No comment provided by engineer. */
+"Chat with member" = "Chat mit einem Mitglied";
+
/* No comment provided by engineer. */
"Chats" = "Chats";
+/* No comment provided by engineer. */
+"Chats with members" = "Chats mit Mitgliedern";
+
/* No comment provided by engineer. */
"Check messages every 20 min." = "Alle 20min Nachrichten überprüfen.";
@@ -1333,9 +1363,15 @@ set passcode view */
/* No comment provided by engineer. */
"Contact already exists" = "Der Kontakt ist bereits vorhanden";
+/* No comment provided by engineer. */
+"contact deleted" = "Kontakt gelöscht";
+
/* No comment provided by engineer. */
"Contact deleted!" = "Kontakt gelöscht!";
+/* No comment provided by engineer. */
+"contact disabled" = "Kontakt deaktiviert";
+
/* No comment provided by engineer. */
"contact has e2e encryption" = "Kontakt nutzt E2E-Verschlüsselung";
@@ -1354,6 +1390,9 @@ set passcode view */
/* No comment provided by engineer. */
"Contact name" = "Kontaktname";
+/* No comment provided by engineer. */
+"contact not ready" = "Kontakt nicht bereit";
+
/* No comment provided by engineer. */
"Contact preferences" = "Kontakt-Präferenzen";
@@ -1602,6 +1641,9 @@ swipe action */
/* No comment provided by engineer. */
"Delete chat profile?" = "Chat-Profil löschen?";
+/* alert title */
+"Delete chat with member?" = "Chat mit dem Mitglied löschen?";
+
/* No comment provided by engineer. */
"Delete chat?" = "Chat löschen?";
@@ -1872,7 +1914,7 @@ swipe action */
/* No comment provided by engineer. */
"Don't miss important messages." = "Verpassen Sie keine wichtigen Nachrichten.";
-/* No comment provided by engineer. */
+/* alert action */
"Don't show again" = "Nicht nochmals anzeigen";
/* No comment provided by engineer. */
@@ -2116,6 +2158,9 @@ chat item action */
/* No comment provided by engineer. */
"Error accepting contact request" = "Fehler beim Annehmen der Kontaktanfrage";
+/* alert title */
+"Error accepting member" = "Fehler beim Übernehmen des Mitglieds";
+
/* No comment provided by engineer. */
"Error adding member(s)" = "Fehler beim Hinzufügen von Mitgliedern";
@@ -2173,6 +2218,9 @@ chat item action */
/* No comment provided by engineer. */
"Error deleting chat database" = "Fehler beim Löschen der Chat-Datenbank";
+/* alert title */
+"Error deleting chat with member" = "Fehler beim Löschen des Chats mit dem Mitglied";
+
/* No comment provided by engineer. */
"Error deleting chat!" = "Fehler beim Löschen des Chats!";
@@ -2236,7 +2284,7 @@ chat item action */
/* alert title */
"Error registering for notifications" = "Fehler beim Registrieren für Benachrichtigungen";
-/* No comment provided by engineer. */
+/* alert title */
"Error removing member" = "Fehler beim Entfernen des Mitglieds";
/* alert title */
@@ -2610,6 +2658,9 @@ snd error text */
/* No comment provided by engineer. */
"Group invitation is no longer valid, it was removed by sender." = "Die Gruppeneinladung ist nicht mehr gültig, da sie vom Absender entfernt wurde.";
+/* No comment provided by engineer. */
+"group is deleted" = "Gruppe wird gelöscht";
+
/* No comment provided by engineer. */
"Group link" = "Gruppen-Link";
@@ -3138,9 +3189,15 @@ snd error text */
/* profile update event chat item */
"member %@ changed to %@" = "Der Mitgliedsname von %1$@ wurde auf %2$@ geändert";
+/* No comment provided by engineer. */
+"Member admission" = "Aufnahme von Mitgliedern";
+
/* rcv group event chat item */
"member connected" = "ist der Gruppe beigetreten";
+/* No comment provided by engineer. */
+"member has old version" = "Das Mitglied hat eine alte App-Version";
+
/* item status text */
"Member inactive" = "Mitglied inaktiv";
@@ -3162,6 +3219,9 @@ snd error text */
/* No comment provided by engineer. */
"Member will be removed from group - this cannot be undone!" = "Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden!";
+/* alert message */
+"Member will join the group, accept member?" = "Ein Mitglied wird der Gruppe beitreten. Übernehmen?";
+
/* No comment provided by engineer. */
"Members can add message reactions." = "Gruppenmitglieder können eine Reaktion auf Nachrichten geben.";
@@ -3432,6 +3492,9 @@ snd error text */
/* No comment provided by engineer. */
"New member role" = "Neue Mitgliedsrolle";
+/* rcv group event chat item */
+"New member wants to join the group." = "Ein neues Mitglied will der Gruppe beitreten.";
+
/* notification */
"new message" = "Neue Nachricht";
@@ -3471,6 +3534,9 @@ snd error text */
/* No comment provided by engineer. */
"No chats in list %@" = "Keine Chats in der Liste %@";
+/* No comment provided by engineer. */
+"No chats with members" = "Keine Chats mit Mitgliedern";
+
/* No comment provided by engineer. */
"No contacts selected" = "Keine Kontakte ausgewählt";
@@ -3555,6 +3621,9 @@ snd error text */
/* No comment provided by engineer. */
"Not compatible!" = "Nicht kompatibel!";
+/* No comment provided by engineer. */
+"not synchronized" = "Nicht synchronisiert";
+
/* No comment provided by engineer. */
"Notes" = "Anmerkungen";
@@ -3587,6 +3656,7 @@ snd error text */
/* enabled status
group pref value
+member criteria value
time to disappear */
"off" = "Aus";
@@ -3599,7 +3669,8 @@ time to disappear */
/* feature offered item */
"offered %@: %@" = "angeboten %1$@: %2$@";
-/* alert button */
+/* alert action
+alert button */
"Ok" = "Ok";
/* No comment provided by engineer. */
@@ -3695,6 +3766,9 @@ time to disappear */
/* No comment provided by engineer. */
"Open group" = "Gruppe öffnen";
+/* alert title */
+"Open link?" = "Link öffnen?";
+
/* authentication reason */
"Open migration to another device" = "Migration auf ein anderes Gerät öffnen";
@@ -3797,6 +3871,9 @@ time to disappear */
/* No comment provided by engineer. */
"pending approval" = "ausstehende Genehmigung";
+/* No comment provided by engineer. */
+"pending review" = "Ausstehende Überprüfung";
+
/* No comment provided by engineer. */
"Periodic" = "Periodisch";
@@ -3866,6 +3943,9 @@ time to disappear */
/* token info */
"Please try to disable and re-enable notfications." = "Bitte versuchen Sie, die Benachrichtigungen zu deaktivieren und wieder zu aktivieren.";
+/* snd group event chat item */
+"Please wait for group moderators to review your request to join the group." = "Bitte warten Sie auf die Überprüfung Ihrer Anfrage durch die Gruppen-Moderatoren, um der Gruppe beitreten zu können.";
+
/* token info */
"Please wait for token activation to complete." = "Bitte warten Sie, bis die Token-Aktivierung abgeschlossen ist.";
@@ -4146,6 +4226,9 @@ swipe action */
/* No comment provided by engineer. */
"Reject contact request" = "Kontaktanfrage ablehnen";
+/* alert title */
+"Reject member?" = "Mitglied ablehnen?";
+
/* No comment provided by engineer. */
"rejected" = "abgelehnt";
@@ -4185,6 +4268,9 @@ swipe action */
/* profile update event chat item */
"removed contact address" = "Die Kontaktadresse wurde entfernt";
+/* No comment provided by engineer. */
+"removed from group" = "Von der Gruppe entfernt";
+
/* profile update event chat item */
"removed profile picture" = "Das Profil-Bild wurde entfernt";
@@ -4233,6 +4319,9 @@ swipe action */
/* No comment provided by engineer. */
"Report reason?" = "Grund der Meldung?";
+/* alert title */
+"Report sent to moderators" = "Meldung wurde an die Moderatoren gesendet";
+
/* report reason */
"Report spam: only group moderators will see it." = "Spam melden: Nur Gruppenmoderatoren werden es sehen.";
@@ -4248,6 +4337,9 @@ swipe action */
/* No comment provided by engineer. */
"Reports" = "Meldungen";
+/* No comment provided by engineer. */
+"request to join rejected" = "Beitrittsanfrage abgelehnt";
+
/* chat list item title */
"requested to connect" = "Zur Verbindung aufgefordert";
@@ -4302,9 +4394,21 @@ swipe action */
/* chat item action */
"Reveal" = "Aufdecken";
+/* No comment provided by engineer. */
+"review" = "Überprüfung";
+
/* No comment provided by engineer. */
"Review conditions" = "Nutzungsbedingungen einsehen";
+/* admission stage */
+"Review members" = "Überprüfung der Mitglieder";
+
+/* admission stage description */
+"Review members before admitting (\"knocking\")." = "Überprüfung der Mitglieder vor der Aufnahme (\"Anklopfen\").";
+
+/* No comment provided by engineer. */
+"reviewed by admins" = "Von Administratoren überprüft";
+
/* No comment provided by engineer. */
"Revoke" = "Widerrufen";
@@ -4333,6 +4437,9 @@ chat item action */
/* alert button */
"Save (and notify contacts)" = "Speichern (und Kontakte benachrichtigen)";
+/* alert title */
+"Save admission settings?" = "Speichern der Aufnahme-Einstellungen?";
+
/* alert button */
"Save and notify contact" = "Speichern und Kontakt benachrichtigen";
@@ -4669,6 +4776,9 @@ chat item action */
/* No comment provided by engineer. */
"Set it instead of system authentication." = "Anstelle der System-Authentifizierung festlegen.";
+/* No comment provided by engineer. */
+"Set member admission" = "Aufnahme von Mitgliedern festlegen";
+
/* No comment provided by engineer. */
"Set message expiration in chats." = "Verfallsdatum von Nachrichten in Chats festlegen.";
@@ -5100,9 +5210,6 @@ report reason */
/* No comment provided by engineer. */
"The old database was not removed during the migration, it can be deleted." = "Die alte Datenbank wurde während der Migration nicht entfernt. Sie kann gelöscht werden.";
-/* No comment provided by engineer. */
-"Your profile is stored on your device and only shared with your contacts." = "Das Profil wird nur mit Ihren Kontakten geteilt.";
-
/* No comment provided by engineer. */
"The same conditions will apply to operator **%@**." = "Dieselben Nutzungsbedingungen gelten auch für den Betreiber **%@**.";
@@ -5712,6 +5819,9 @@ report reason */
/* No comment provided by engineer. */
"You accepted connection" = "Sie haben die Verbindung akzeptiert";
+/* snd group event chat item */
+"you accepted this member" = "Sie haben dieses Mitglied übernommen";
+
/* No comment provided by engineer. */
"You allow" = "Sie erlauben";
@@ -5823,6 +5933,9 @@ report reason */
/* alert message */
"You can view invitation link again in connection details." = "Den Einladungslink können Sie in den Details der Verbindung nochmals sehen.";
+/* alert message */
+"You can view your reports in Chat with admins." = "Sie können Ihre Meldungen im Chat mit den Administratoren sehen.";
+
/* No comment provided by engineer. */
"You can't send messages!" = "Sie können keine Nachrichten versenden!";
@@ -5991,15 +6104,15 @@ report reason */
/* No comment provided by engineer. */
"Your profile **%@** will be shared." = "Ihr Profil **%@** wird geteilt.";
+/* No comment provided by engineer. */
+"Your profile is stored on your device and only shared with your contacts." = "Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt.";
+
/* No comment provided by engineer. */
"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt. SimpleX-Server können Ihr Profil nicht einsehen.";
/* alert message */
"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Ihr Profil wurde geändert. Wenn Sie es speichern, wird das aktualisierte Profil an alle Ihre Kontakte gesendet.";
-/* No comment provided by engineer. */
-"Your profile, contacts and delivered messages are stored on your device." = "Ihr Profil, Ihre Kontakte und zugestellten Nachrichten werden auf Ihrem Gerät gespeichert.";
-
/* No comment provided by engineer. */
"Your random profile" = "Ihr Zufallsprofil";
diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings
index 28ba0f0642..a8782c401f 100644
--- a/apps/ios/es.lproj/Localizable.strings
+++ b/apps/ios/es.lproj/Localizable.strings
@@ -1872,7 +1872,7 @@ swipe action */
/* No comment provided by engineer. */
"Don't miss important messages." = "No pierdas los mensajes importantes.";
-/* No comment provided by engineer. */
+/* alert action */
"Don't show again" = "No volver a mostrar";
/* No comment provided by engineer. */
@@ -2236,7 +2236,7 @@ chat item action */
/* alert title */
"Error registering for notifications" = "Error al registrarse para notificaciones";
-/* No comment provided by engineer. */
+/* alert title */
"Error removing member" = "Error al expulsar miembro";
/* alert title */
@@ -3587,6 +3587,7 @@ snd error text */
/* enabled status
group pref value
+member criteria value
time to disappear */
"off" = "desactivado";
@@ -3599,7 +3600,8 @@ time to disappear */
/* feature offered item */
"offered %@: %@" = "ofrecido %1$@: %2$@";
-/* alert button */
+/* alert action
+alert button */
"Ok" = "Ok";
/* No comment provided by engineer. */
@@ -5100,9 +5102,6 @@ report reason */
/* No comment provided by engineer. */
"The old database was not removed during the migration, it can be deleted." = "La base de datos antigua no se eliminó durante la migración, puede eliminarse.";
-/* No comment provided by engineer. */
-"Your profile is stored on your device and only shared with your contacts." = "El perfil sólo se comparte con tus contactos.";
-
/* No comment provided by engineer. */
"The same conditions will apply to operator **%@**." = "Las mismas condiciones se aplicarán al operador **%@**.";
@@ -5991,15 +5990,15 @@ report reason */
/* No comment provided by engineer. */
"Your profile **%@** will be shared." = "El perfil **%@** será compartido.";
+/* No comment provided by engineer. */
+"Your profile is stored on your device and only shared with your contacts." = "El perfil sólo se comparte con tus contactos.";
+
/* No comment provided by engineer. */
"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil.";
/* alert message */
"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Tu perfil ha sido modificado. Si lo guardas la actualización será enviada a todos tus contactos.";
-/* No comment provided by engineer. */
-"Your profile, contacts and delivered messages are stored on your device." = "Tu perfil, contactos y mensajes se almacenan en tu dispositivo.";
-
/* No comment provided by engineer. */
"Your random profile" = "Tu perfil aleatorio";
diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings
index 4891c7fb26..8e489f7a71 100644
--- a/apps/ios/fi.lproj/Localizable.strings
+++ b/apps/ios/fi.lproj/Localizable.strings
@@ -1073,7 +1073,7 @@ swipe action */
/* No comment provided by engineer. */
"Don't enable" = "Älä salli";
-/* No comment provided by engineer. */
+/* alert action */
"Don't show again" = "Älä näytä uudelleen";
/* No comment provided by engineer. */
@@ -1307,7 +1307,7 @@ swipe action */
/* alert title */
"Error receiving file" = "Virhe tiedoston vastaanottamisessa";
-/* No comment provided by engineer. */
+/* alert title */
"Error removing member" = "Virhe poistettaessa jäsentä";
/* No comment provided by engineer. */
@@ -2079,6 +2079,7 @@ snd error text */
/* enabled status
group pref value
+member criteria value
time to disappear */
"off" = "pois";
@@ -2091,7 +2092,8 @@ time to disappear */
/* feature offered item */
"offered %@: %@" = "tarjottu %1$@: %2$@";
-/* alert button */
+/* alert action
+alert button */
"Ok" = "Ok";
/* No comment provided by engineer. */
@@ -2910,9 +2912,6 @@ chat item action */
/* No comment provided by engineer. */
"The old database was not removed during the migration, it can be deleted." = "Vanhaa tietokantaa ei poistettu siirron aikana, se voidaan kuitenkin poistaa.";
-/* No comment provided by engineer. */
-"Your profile is stored on your device and only shared with your contacts." = "Profiili jaetaan vain kontaktiesi kanssa.";
-
/* No comment provided by engineer. */
"The second tick we missed! ✅" = "Toinen kuittaus, joka uupui! ✅";
@@ -3391,10 +3390,10 @@ chat item action */
"Your profile **%@** will be shared." = "Profiilisi **%@** jaetaan.";
/* No comment provided by engineer. */
-"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa. SimpleX-palvelimet eivät näe profiiliasi.";
+"Your profile is stored on your device and only shared with your contacts." = "Profiili jaetaan vain kontaktiesi kanssa.";
/* No comment provided by engineer. */
-"Your profile, contacts and delivered messages are stored on your device." = "Profiilisi, kontaktisi ja toimitetut viestit tallennetaan laitteellesi.";
+"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa. SimpleX-palvelimet eivät näe profiiliasi.";
/* No comment provided by engineer. */
"Your random profile" = "Satunnainen profiilisi";
diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings
index 4dd75039dc..9b570a5ae9 100644
--- a/apps/ios/fr.lproj/Localizable.strings
+++ b/apps/ios/fr.lproj/Localizable.strings
@@ -1863,7 +1863,7 @@ swipe action */
/* No comment provided by engineer. */
"Don't miss important messages." = "Ne manquez pas les messages importants.";
-/* No comment provided by engineer. */
+/* alert action */
"Don't show again" = "Ne plus afficher";
/* No comment provided by engineer. */
@@ -2227,7 +2227,7 @@ chat item action */
/* alert title */
"Error registering for notifications" = "Erreur lors de l'inscription aux notifications";
-/* No comment provided by engineer. */
+/* alert title */
"Error removing member" = "Erreur lors de la suppression d'un membre";
/* alert title */
@@ -3479,6 +3479,7 @@ snd error text */
/* enabled status
group pref value
+member criteria value
time to disappear */
"off" = "off";
@@ -3491,7 +3492,8 @@ time to disappear */
/* feature offered item */
"offered %@: %@" = "propose %1$@ : %2$@";
-/* alert button */
+/* alert action
+alert button */
"Ok" = "Ok";
/* No comment provided by engineer. */
@@ -4883,9 +4885,6 @@ chat item action */
/* No comment provided by engineer. */
"The old database was not removed during the migration, it can be deleted." = "L'ancienne base de données n'a pas été supprimée lors de la migration, elle peut être supprimée.";
-/* No comment provided by engineer. */
-"Your profile is stored on your device and only shared with your contacts." = "Le profil n'est partagé qu'avec vos contacts.";
-
/* No comment provided by engineer. */
"The same conditions will apply to operator **%@**." = "Les mêmes conditions s'appliquent à l'opérateur **%@**.";
@@ -5741,15 +5740,15 @@ chat item action */
/* No comment provided by engineer. */
"Your profile **%@** will be shared." = "Votre profil **%@** sera partagé.";
+/* No comment provided by engineer. */
+"Your profile is stored on your device and only shared with your contacts." = "Le profil n'est partagé qu'avec vos contacts.";
+
/* No comment provided by engineer. */
"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Votre profil est stocké sur votre appareil et est seulement partagé avec vos contacts. Les serveurs SimpleX ne peuvent pas voir votre profil.";
/* alert message */
"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Votre profil a été modifié. Si vous l'enregistrez, le profil mis à jour sera envoyé à tous vos contacts.";
-/* No comment provided by engineer. */
-"Your profile, contacts and delivered messages are stored on your device." = "Votre profil, vos contacts et les messages reçus sont stockés sur votre appareil.";
-
/* No comment provided by engineer. */
"Your random profile" = "Votre profil aléatoire";
diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings
index 5a9b6b4e38..9a1da01665 100644
--- a/apps/ios/hu.lproj/Localizable.strings
+++ b/apps/ios/hu.lproj/Localizable.strings
@@ -345,6 +345,12 @@ accept incoming call via notification
swipe action */
"Accept" = "Elfogadás";
+/* alert action */
+"Accept as member" = "Befogadás tagként";
+
+/* alert action */
+"Accept as observer" = "Befogadás megfigyelőként";
+
/* No comment provided by engineer. */
"Accept conditions" = "Feltételek elfogadása";
@@ -358,6 +364,12 @@ swipe action */
swipe action */
"Accept incognito" = "Elfogadás inkognitóban";
+/* alert title */
+"Accept member" = "Tag befogadása";
+
+/* rcv group event chat item */
+"accepted %@" = "befogadta őt: %@";
+
/* call status */
"accepted call" = "fogadott hívás";
@@ -367,6 +379,9 @@ swipe action */
/* chat list item title */
"accepted invitation" = "elfogadott meghívó";
+/* rcv group event chat item */
+"accepted you" = "befogadta Önt";
+
/* No comment provided by engineer. */
"Acknowledged" = "Visszaigazolt";
@@ -463,6 +478,9 @@ swipe action */
/* chat item text */
"agreeing encryption…" = "titkosítás elfogadása…";
+/* member criteria value */
+"all" = "összes";
+
/* No comment provided by engineer. */
"All" = "Összes";
@@ -905,6 +923,9 @@ marked deleted chat item preview text */
/* No comment provided by engineer. */
"Can't message member" = "Nem lehet üzenetet küldeni a tagnak";
+/* No comment provided by engineer. */
+"can't send messages" = "nem lehet üzeneteket küldeni";
+
/* alert action
alert button */
"Cancel" = "Mégse";
@@ -1042,9 +1063,18 @@ set passcode view */
/* No comment provided by engineer. */
"Chat will be deleted for you - this cannot be undone!" = "A csevegés törölve lesz az Ön számára – ez a művelet nem vonható vissza!";
+/* chat toolbar */
+"Chat with admins" = "Csevegés az adminisztrátorokkal";
+
+/* No comment provided by engineer. */
+"Chat with member" = "Csevegés a taggal";
+
/* No comment provided by engineer. */
"Chats" = "Csevegések";
+/* No comment provided by engineer. */
+"Chats with members" = "Csevegés a tagokkal";
+
/* No comment provided by engineer. */
"Check messages every 20 min." = "Üzenetek ellenőrzése 20 percenként.";
@@ -1333,9 +1363,15 @@ set passcode view */
/* No comment provided by engineer. */
"Contact already exists" = "A partner már létezik";
+/* No comment provided by engineer. */
+"contact deleted" = "partner törölve";
+
/* No comment provided by engineer. */
"Contact deleted!" = "Partner törölve!";
+/* No comment provided by engineer. */
+"contact disabled" = "partner letiltva";
+
/* No comment provided by engineer. */
"contact has e2e encryption" = "a partner e2e titkosítással rendelkezik";
@@ -1354,6 +1390,9 @@ set passcode view */
/* No comment provided by engineer. */
"Contact name" = "Csak név";
+/* No comment provided by engineer. */
+"contact not ready" = "a kapcsolat nem áll készen";
+
/* No comment provided by engineer. */
"Contact preferences" = "Partnerbeállítások";
@@ -1505,7 +1544,7 @@ set passcode view */
"Database ID: %d" = "Adatbázis-azonosító: %d";
/* No comment provided by engineer. */
-"Database IDs and Transport isolation option." = "Adatbázis-azonosítók és átvitel-izolációs beállítások.";
+"Database IDs and Transport isolation option." = "Adatbázis-azonosítók és átvitelelkülönítési beállítások.";
/* No comment provided by engineer. */
"Database is encrypted using a random passphrase, you can change it." = "Az adatbázis egy véletlenszerű jelmondattal van titkosítva, amelyet szabadon módosíthat.";
@@ -1550,7 +1589,7 @@ set passcode view */
"Decentralized" = "Decentralizált";
/* message decrypt error item */
-"Decryption error" = "Titkosítás visszafejtési hiba";
+"Decryption error" = "Titkosításvisszafejtési hiba";
/* No comment provided by engineer. */
"decryption errors" = "visszafejtési hibák";
@@ -1602,6 +1641,9 @@ swipe action */
/* No comment provided by engineer. */
"Delete chat profile?" = "Törli a csevegési profilt?";
+/* alert title */
+"Delete chat with member?" = "Törli a taggal való csevegést?";
+
/* No comment provided by engineer. */
"Delete chat?" = "Törli a csevegést?";
@@ -1777,7 +1819,7 @@ swipe action */
"different migration in the app/database: %@ / %@" = "különböző átköltöztetés az alkalmazásban/adatbázisban: %@ / %@";
/* No comment provided by engineer. */
-"Different names, avatars and transport isolation." = "Különböző nevek, profilképek és átvitel-izoláció.";
+"Different names, avatars and transport isolation." = "Különböző nevek, profilképek és átvitelizoláció.";
/* connection level description */
"direct" = "közvetlen";
@@ -1872,7 +1914,7 @@ swipe action */
/* No comment provided by engineer. */
"Don't miss important messages." = "Ne maradjon le a fontos üzenetekről.";
-/* No comment provided by engineer. */
+/* alert action */
"Don't show again" = "Ne mutasd újra";
/* No comment provided by engineer. */
@@ -2116,6 +2158,9 @@ chat item action */
/* No comment provided by engineer. */
"Error accepting contact request" = "Hiba történt a meghívási kérés elfogadásakor";
+/* alert title */
+"Error accepting member" = "Hiba a tag befogadásakor";
+
/* No comment provided by engineer. */
"Error adding member(s)" = "Hiba történt a tag(ok) hozzáadásakor";
@@ -2173,6 +2218,9 @@ chat item action */
/* No comment provided by engineer. */
"Error deleting chat database" = "Hiba történt a csevegési adatbázis törlésekor";
+/* alert title */
+"Error deleting chat with member" = "Hiba a taggal való csevegés törlésekor";
+
/* No comment provided by engineer. */
"Error deleting chat!" = "Hiba történt a csevegés törlésekor!";
@@ -2189,7 +2237,7 @@ chat item action */
"Error deleting token" = "Hiba történt a token törlésekor";
/* No comment provided by engineer. */
-"Error deleting user profile" = "Hiba történt a felhasználó-profil törlésekor";
+"Error deleting user profile" = "Hiba történt a felhasználói profil törlésekor";
/* No comment provided by engineer. */
"Error downloading the archive" = "Hiba történt az archívum letöltésekor";
@@ -2236,7 +2284,7 @@ chat item action */
/* alert title */
"Error registering for notifications" = "Hiba történt az értesítések regisztrálásakor";
-/* No comment provided by engineer. */
+/* alert title */
"Error removing member" = "Hiba történt a tag eltávolításakor";
/* alert title */
@@ -2610,6 +2658,9 @@ snd error text */
/* No comment provided by engineer. */
"Group invitation is no longer valid, it was removed by sender." = "A csoportmeghívó már nem érvényes, a küldője eltávolította.";
+/* No comment provided by engineer. */
+"group is deleted" = "csoport törölve";
+
/* No comment provided by engineer. */
"Group link" = "Csoporthivatkozás";
@@ -2956,10 +3007,10 @@ snd error text */
"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Lehetővé teszi, hogy egyetlen csevegési profilon belül több névtelen kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük.";
/* No comment provided by engineer. */
-"It can happen when you or your connection used the old database backup." = "Ez akkor fordulhat elő, ha Ön vagy a partnere régi adatbázis biztonsági mentést használt.";
+"It can happen when you or your connection used the old database backup." = "Ez akkor fordulhat elő, ha Ön vagy a partnere egy régi adatbázis biztonsági mentését használta.";
/* No comment provided by engineer. */
-"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Nem sikerült az üzenetet visszafejteni, mert Ön, vagy a partnere régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült.";
+"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Nem sikerült az üzenetet visszafejteni, mert Ön, vagy a partnere egy régi adatbázis biztonsági mentését használta.\n3. A kapcsolat sérült.";
/* No comment provided by engineer. */
"It protects your IP address and connections." = "Védi az IP-címét és a kapcsolatait.";
@@ -3138,9 +3189,15 @@ snd error text */
/* profile update event chat item */
"member %@ changed to %@" = "%1$@ a következőre módosította a nevét: %2$@";
+/* No comment provided by engineer. */
+"Member admission" = "Tagbefogadás";
+
/* rcv group event chat item */
"member connected" = "kapcsolódott";
+/* No comment provided by engineer. */
+"member has old version" = "a tag régi verziót használ";
+
/* item status text */
"Member inactive" = "Inaktív tag";
@@ -3162,6 +3219,9 @@ snd error text */
/* No comment provided by engineer. */
"Member will be removed from group - this cannot be undone!" = "A tag el lesz távolítva a csoportból – ez a művelet nem vonható vissza!";
+/* alert message */
+"Member will join the group, accept member?" = "A tag csatlakozni akar a csoporthoz, befogadja a tagot?";
+
/* No comment provided by engineer. */
"Members can add message reactions." = "A tagok reakciókat adhatnak hozzá az üzenetekhez.";
@@ -3274,10 +3334,10 @@ snd error text */
"Messages were deleted after you selected them." = "Az üzeneteket törölték miután kijelölte őket.";
/* No comment provided by engineer. */
-"Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzenetek, a fájlok és a hívások **végpontok közötti titkosítással**, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve.";
+"Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzenetek, a fájlok és a hívások **végpontok közötti titkosítással**, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve.";
/* No comment provided by engineer. */
-"Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzenetek, a fájlok és a hívások **végpontok közötti kvantumbiztos titkosítással**, sérülés utáni titkosságvédelemmel és -helyreállítással, továbbá letagadhatósággal vannak védve.";
+"Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzenetek, a fájlok és a hívások **végpontok közötti kvantumbiztos titkosítással**, kompromittálás előtti és utáni titkosságvédelemmel, illetve letagadhatósággal vannak védve.";
/* No comment provided by engineer. */
"Migrate device" = "Eszköz átköltöztetése";
@@ -3432,6 +3492,9 @@ snd error text */
/* No comment provided by engineer. */
"New member role" = "Új tag szerepköre";
+/* rcv group event chat item */
+"New member wants to join the group." = "Új tag szeretne csatlakozni a csoporthoz.";
+
/* notification */
"new message" = "új üzenet";
@@ -3471,6 +3534,9 @@ snd error text */
/* No comment provided by engineer. */
"No chats in list %@" = "Nincsenek csevegések a(z) %@ nevű listában";
+/* No comment provided by engineer. */
+"No chats with members" = "Nincsenek csevegések a tagokkal";
+
/* No comment provided by engineer. */
"No contacts selected" = "Nincs partner kijelölve";
@@ -3550,11 +3616,14 @@ snd error text */
"No unread chats" = "Nincsenek olvasatlan csevegések";
/* No comment provided by engineer. */
-"No user identifiers." = "Nincsenek felhasználó-azonosítók.";
+"No user identifiers." = "Nincsenek felhasználói azonosítók.";
/* No comment provided by engineer. */
"Not compatible!" = "Nem kompatibilis!";
+/* No comment provided by engineer. */
+"not synchronized" = "nincs szinkronizálva";
+
/* No comment provided by engineer. */
"Notes" = "Jegyzetek";
@@ -3587,6 +3656,7 @@ snd error text */
/* enabled status
group pref value
+member criteria value
time to disappear */
"off" = "kikapcsolva";
@@ -3599,7 +3669,8 @@ time to disappear */
/* feature offered item */
"offered %@: %@" = "ajánlotta: %1$@, ekkor: %2$@";
-/* alert button */
+/* alert action
+alert button */
"Ok" = "Rendben";
/* No comment provided by engineer. */
@@ -3695,6 +3766,9 @@ time to disappear */
/* No comment provided by engineer. */
"Open group" = "Csoport megnyitása";
+/* alert title */
+"Open link?" = "Megnyitja a hivatkozást?";
+
/* authentication reason */
"Open migration to another device" = "Átköltöztetés indítása egy másik eszközre";
@@ -3797,6 +3871,9 @@ time to disappear */
/* No comment provided by engineer. */
"pending approval" = "jóváhagyásra vár";
+/* No comment provided by engineer. */
+"pending review" = "függőben lévő áttekintés";
+
/* No comment provided by engineer. */
"Periodic" = "Időszakos";
@@ -3866,6 +3943,9 @@ time to disappear */
/* token info */
"Please try to disable and re-enable notfications." = "Próbálja meg letiltani és újra engedélyezni az értesítéseket.";
+/* snd group event chat item */
+"Please wait for group moderators to review your request to join the group." = "Várja meg, amíg a csoport moderátorai áttekintik a csoporthoz való csatlakozási kérelmét.";
+
/* token info */
"Please wait for token activation to complete." = "Várjon, amíg a token aktiválása befejeződik.";
@@ -4146,6 +4226,9 @@ swipe action */
/* No comment provided by engineer. */
"Reject contact request" = "Meghívási kérés elutasítása";
+/* alert title */
+"Reject member?" = "Elutasítja a tagot?";
+
/* No comment provided by engineer. */
"rejected" = "elutasítva";
@@ -4153,10 +4236,10 @@ swipe action */
"rejected call" = "elutasított hívás";
/* No comment provided by engineer. */
-"Relay server is only used if necessary. Another party can observe your IP address." = "A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címet.";
+"Relay server is only used if necessary. Another party can observe your IP address." = "A továbbítókiszolgáló csak szükség esetén lesz használva. Egy másik fél megfigyelheti az IP-címét.";
/* No comment provided by engineer. */
-"Relay server protects your IP address, but it can observe the duration of the call." = "A továbbítókiszolgáló megvédi az Ön IP-címét, de megfigyelheti a hívás időtartamát.";
+"Relay server protects your IP address, but it can observe the duration of the call." = "A továbbítókiszolgáló megvédi az IP-címét, de megfigyelheti a hívás időtartamát.";
/* No comment provided by engineer. */
"Remove" = "Eltávolítás";
@@ -4185,6 +4268,9 @@ swipe action */
/* profile update event chat item */
"removed contact address" = "eltávolította a kapcsolattartási címet";
+/* No comment provided by engineer. */
+"removed from group" = "eltávolítva a csoportból";
+
/* profile update event chat item */
"removed profile picture" = "eltávolította a profilképét";
@@ -4233,6 +4319,9 @@ swipe action */
/* No comment provided by engineer. */
"Report reason?" = "Jelentés indoklása?";
+/* alert title */
+"Report sent to moderators" = "A jelentés el lett küldve a moderátoroknak";
+
/* report reason */
"Report spam: only group moderators will see it." = "Kéretlen tartalom jelentése: csak a csoport moderátorai látják.";
@@ -4248,6 +4337,9 @@ swipe action */
/* No comment provided by engineer. */
"Reports" = "Jelentések";
+/* No comment provided by engineer. */
+"request to join rejected" = "csatlakozási kérelem elutasítva";
+
/* chat list item title */
"requested to connect" = "Függőben lévő meghívási kérelem";
@@ -4302,9 +4394,21 @@ swipe action */
/* chat item action */
"Reveal" = "Felfedés";
+/* No comment provided by engineer. */
+"review" = "áttekintés";
+
/* No comment provided by engineer. */
"Review conditions" = "Feltételek felülvizsgálata";
+/* admission stage */
+"Review members" = "Tagok áttekintése";
+
+/* admission stage description */
+"Review members before admitting (\"knocking\")." = "Tagok áttekintése a befogadás előtt (kopogtatás).";
+
+/* No comment provided by engineer. */
+"reviewed by admins" = "áttekintve a moderátorok által";
+
/* No comment provided by engineer. */
"Revoke" = "Visszavonás";
@@ -4333,6 +4437,9 @@ chat item action */
/* alert button */
"Save (and notify contacts)" = "Mentés (és a partnerek értesítése)";
+/* alert title */
+"Save admission settings?" = "Elmenti a befogadási beállításokat?";
+
/* alert button */
"Save and notify contact" = "Mentés és a partner értesítése";
@@ -4669,6 +4776,9 @@ chat item action */
/* No comment provided by engineer. */
"Set it instead of system authentication." = "Beállítás a rendszer-hitelesítés helyett.";
+/* No comment provided by engineer. */
+"Set member admission" = "Tagbefogadás beállítása";
+
/* No comment provided by engineer. */
"Set message expiration in chats." = "Üzenetek eltűnési idejének módosítása a csevegésekben.";
@@ -4830,7 +4940,7 @@ chat item action */
"SimpleX one-time invitation" = "Egyszer használható SimpleX-meghívó";
/* No comment provided by engineer. */
-"SimpleX protocols reviewed by Trail of Bits." = "A SimpleX Chat biztonsága a Trail of Bits által lett felülvizsgálva.";
+"SimpleX protocols reviewed by Trail of Bits." = "A SimpleX-protokollokat a Trail of Bits auditálta.";
/* No comment provided by engineer. */
"Simplified incognito mode" = "Egyszerűsített inkognitómód";
@@ -5100,9 +5210,6 @@ report reason */
/* No comment provided by engineer. */
"The old database was not removed during the migration, it can be deleted." = "A régi adatbázis nem lett eltávolítva az átköltöztetéskor, ezért törölhető.";
-/* No comment provided by engineer. */
-"Your profile is stored on your device and only shared with your contacts." = "A profilja csak a partnereivel van megosztva.";
-
/* No comment provided by engineer. */
"The same conditions will apply to operator **%@**." = "Ugyanezek a feltételek lesznek elfogadva a következő üzemeltető számára is: **%@**.";
@@ -5263,7 +5370,7 @@ report reason */
"Total" = "Összes kapcsolat";
/* No comment provided by engineer. */
-"Transport isolation" = "Átvitel-izoláció";
+"Transport isolation" = "Átvitelelkülönítés";
/* No comment provided by engineer. */
"Transport sessions" = "Munkamenetek átvitele";
@@ -5458,7 +5565,7 @@ report reason */
"Use only local notifications?" = "Csak helyi értesítések használata?";
/* No comment provided by engineer. */
-"Use private routing with unknown servers when IP address is not protected." = "Használjon privát útválasztást ismeretlen kiszolgálókkal, ha az IP-cím nem védett.";
+"Use private routing with unknown servers when IP address is not protected." = "Használjon privát útválasztást az ismeretlen kiszolgálókkal, ha az IP-cím nem védett.";
/* No comment provided by engineer. */
"Use private routing with unknown servers." = "Használjon privát útválasztást ismeretlen kiszolgálókkal.";
@@ -5680,10 +5787,10 @@ report reason */
"With reduced battery usage." = "Csökkentett akkumulátor-használattal.";
/* No comment provided by engineer. */
-"Without Tor or VPN, your IP address will be visible to file servers." = "Tor vagy VPN nélkül az Ön IP-címe látható lesz a fájlkiszolgálók számára.";
+"Without Tor or VPN, your IP address will be visible to file servers." = "Tor vagy VPN nélkül az IP-címe láthatóvá válik a fájlkiszolgálók számára.";
/* alert message */
-"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Tor vagy VPN nélkül az Ön IP-címe látható lesz a következő XFTP-továbbítókiszolgálók számára: %@.";
+"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Tor vagy VPN nélkül az IP-címe láthatóvá válik a következő XFTP-továbbítókiszolgálók számára: %@.";
/* No comment provided by engineer. */
"Wrong database passphrase" = "Érvénytelen adatbázis-jelmondat";
@@ -5712,6 +5819,9 @@ report reason */
/* No comment provided by engineer. */
"You accepted connection" = "Kapcsolat létrehozása";
+/* snd group event chat item */
+"you accepted this member" = "Ön befogadta ezt a tagot";
+
/* No comment provided by engineer. */
"You allow" = "Ön engedélyezi";
@@ -5823,6 +5933,9 @@ report reason */
/* alert message */
"You can view invitation link again in connection details." = "A meghívási hivatkozást újra megtekintheti a kapcsolat részleteinél.";
+/* alert message */
+"You can view your reports in Chat with admins." = "A jelentéseket megtekintheti a „Csevegés az adminisztrátorokkal” menüben.";
+
/* No comment provided by engineer. */
"You can't send messages!" = "Nem lehet üzeneteket küldeni!";
@@ -5991,15 +6104,15 @@ report reason */
/* No comment provided by engineer. */
"Your profile **%@** will be shared." = "A(z) **%@** nevű profilja meg lesz osztva.";
+/* No comment provided by engineer. */
+"Your profile is stored on your device and only shared with your contacts." = "A profilja az eszközén van tárolva és csak a partnereivel van megosztva.";
+
/* No comment provided by engineer. */
"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "A profilja az eszközén van tárolva és csak a partnereivel van megosztva. A SimpleX-kiszolgálók nem láthatják a profilját.";
/* alert message */
"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "A profilja módosult. Ha elmenti, a profilfrissítés el lesz küldve a partnerei számára.";
-/* No comment provided by engineer. */
-"Your profile, contacts and delivered messages are stored on your device." = "A profilja, a partnerei és az elküldött üzenetei a saját eszközén vannak tárolva.";
-
/* No comment provided by engineer. */
"Your random profile" = "Véletlenszerű profil";
diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings
index b914a06079..f36b35efc2 100644
--- a/apps/ios/it.lproj/Localizable.strings
+++ b/apps/ios/it.lproj/Localizable.strings
@@ -345,6 +345,12 @@ accept incoming call via notification
swipe action */
"Accept" = "Accetta";
+/* alert action */
+"Accept as member" = "Accetta come membro";
+
+/* alert action */
+"Accept as observer" = "Accetta come osservatore";
+
/* No comment provided by engineer. */
"Accept conditions" = "Accetta le condizioni";
@@ -358,6 +364,12 @@ swipe action */
swipe action */
"Accept incognito" = "Accetta in incognito";
+/* alert title */
+"Accept member" = "Accetta membro";
+
+/* rcv group event chat item */
+"accepted %@" = "%@ accettato";
+
/* call status */
"accepted call" = "chiamata accettata";
@@ -367,6 +379,9 @@ swipe action */
/* chat list item title */
"accepted invitation" = "invito accettato";
+/* rcv group event chat item */
+"accepted you" = "ti ha accettato/a";
+
/* No comment provided by engineer. */
"Acknowledged" = "Riconosciuto";
@@ -463,6 +478,9 @@ swipe action */
/* chat item text */
"agreeing encryption…" = "concordando la crittografia…";
+/* member criteria value */
+"all" = "tutti";
+
/* No comment provided by engineer. */
"All" = "Tutte";
@@ -905,6 +923,9 @@ marked deleted chat item preview text */
/* No comment provided by engineer. */
"Can't message member" = "Impossibile inviare un messaggio al membro";
+/* No comment provided by engineer. */
+"can't send messages" = "impossibile inviare messaggi";
+
/* alert action
alert button */
"Cancel" = "Annulla";
@@ -1042,9 +1063,18 @@ set passcode view */
/* No comment provided by engineer. */
"Chat will be deleted for you - this cannot be undone!" = "La chat verrà eliminata solo per te, non è reversibile!";
+/* chat toolbar */
+"Chat with admins" = "Chat con amministratori";
+
+/* No comment provided by engineer. */
+"Chat with member" = "Chatta con il membro";
+
/* No comment provided by engineer. */
"Chats" = "Chat";
+/* No comment provided by engineer. */
+"Chats with members" = "Chat con membri";
+
/* No comment provided by engineer. */
"Check messages every 20 min." = "Controlla i messaggi ogni 20 min.";
@@ -1333,9 +1363,15 @@ set passcode view */
/* No comment provided by engineer. */
"Contact already exists" = "Il contatto esiste già";
+/* No comment provided by engineer. */
+"contact deleted" = "contatto eliminato";
+
/* No comment provided by engineer. */
"Contact deleted!" = "Contatto eliminato!";
+/* No comment provided by engineer. */
+"contact disabled" = "contatto disattivato";
+
/* No comment provided by engineer. */
"contact has e2e encryption" = "il contatto ha la crittografia e2e";
@@ -1354,6 +1390,9 @@ set passcode view */
/* No comment provided by engineer. */
"Contact name" = "Nome del contatto";
+/* No comment provided by engineer. */
+"contact not ready" = "contatto non pronto";
+
/* No comment provided by engineer. */
"Contact preferences" = "Preferenze del contatto";
@@ -1602,6 +1641,9 @@ swipe action */
/* No comment provided by engineer. */
"Delete chat profile?" = "Eliminare il profilo di chat?";
+/* alert title */
+"Delete chat with member?" = "Eliminare la chat con il membro?";
+
/* No comment provided by engineer. */
"Delete chat?" = "Eliminare la chat?";
@@ -1872,7 +1914,7 @@ swipe action */
/* No comment provided by engineer. */
"Don't miss important messages." = "Non perdere messaggi importanti.";
-/* No comment provided by engineer. */
+/* alert action */
"Don't show again" = "Non mostrare più";
/* No comment provided by engineer. */
@@ -2116,6 +2158,9 @@ chat item action */
/* No comment provided by engineer. */
"Error accepting contact request" = "Errore nell'accettazione della richiesta di contatto";
+/* alert title */
+"Error accepting member" = "Errore di accettazione del membro";
+
/* No comment provided by engineer. */
"Error adding member(s)" = "Errore di aggiunta membro/i";
@@ -2173,6 +2218,9 @@ chat item action */
/* No comment provided by engineer. */
"Error deleting chat database" = "Errore nell'eliminazione del database della chat";
+/* alert title */
+"Error deleting chat with member" = "Errore di eliminazione della chat con il membro";
+
/* No comment provided by engineer. */
"Error deleting chat!" = "Errore nell'eliminazione della chat!";
@@ -2236,7 +2284,7 @@ chat item action */
/* alert title */
"Error registering for notifications" = "Errore di registrazione per le notifiche";
-/* No comment provided by engineer. */
+/* alert title */
"Error removing member" = "Errore nella rimozione del membro";
/* alert title */
@@ -2610,6 +2658,9 @@ snd error text */
/* No comment provided by engineer. */
"Group invitation is no longer valid, it was removed by sender." = "L'invito al gruppo non è più valido, è stato rimosso dal mittente.";
+/* No comment provided by engineer. */
+"group is deleted" = "il gruppo è eliminato";
+
/* No comment provided by engineer. */
"Group link" = "Link del gruppo";
@@ -3138,9 +3189,15 @@ snd error text */
/* profile update event chat item */
"member %@ changed to %@" = "il membro %1$@ è diventato %2$@";
+/* No comment provided by engineer. */
+"Member admission" = "Ammissione del membro";
+
/* rcv group event chat item */
"member connected" = "si è connesso/a";
+/* No comment provided by engineer. */
+"member has old version" = "il membro ha una versione vecchia";
+
/* item status text */
"Member inactive" = "Membro inattivo";
@@ -3162,6 +3219,9 @@ snd error text */
/* No comment provided by engineer. */
"Member will be removed from group - this cannot be undone!" = "Il membro verrà rimosso dal gruppo, non è reversibile!";
+/* alert message */
+"Member will join the group, accept member?" = "Il membro entrerà nel gruppo, accettarlo?";
+
/* No comment provided by engineer. */
"Members can add message reactions." = "I membri del gruppo possono aggiungere reazioni ai messaggi.";
@@ -3432,6 +3492,9 @@ snd error text */
/* No comment provided by engineer. */
"New member role" = "Nuovo ruolo del membro";
+/* rcv group event chat item */
+"New member wants to join the group." = "Un nuovo membro vuole entrare nel gruppo.";
+
/* notification */
"new message" = "messaggio nuovo";
@@ -3471,6 +3534,9 @@ snd error text */
/* No comment provided by engineer. */
"No chats in list %@" = "Nessuna chat nell'elenco %@";
+/* No comment provided by engineer. */
+"No chats with members" = "Nessuna chat con membri";
+
/* No comment provided by engineer. */
"No contacts selected" = "Nessun contatto selezionato";
@@ -3555,6 +3621,9 @@ snd error text */
/* No comment provided by engineer. */
"Not compatible!" = "Non compatibile!";
+/* No comment provided by engineer. */
+"not synchronized" = "non sincronizzato";
+
/* No comment provided by engineer. */
"Notes" = "Note";
@@ -3587,6 +3656,7 @@ snd error text */
/* enabled status
group pref value
+member criteria value
time to disappear */
"off" = "off";
@@ -3599,7 +3669,8 @@ time to disappear */
/* feature offered item */
"offered %@: %@" = "offerto %1$@: %2$@";
-/* alert button */
+/* alert action
+alert button */
"Ok" = "Ok";
/* No comment provided by engineer. */
@@ -3695,6 +3766,9 @@ time to disappear */
/* No comment provided by engineer. */
"Open group" = "Apri gruppo";
+/* alert title */
+"Open link?" = "Aprire il link?";
+
/* authentication reason */
"Open migration to another device" = "Apri migrazione ad un altro dispositivo";
@@ -3797,6 +3871,9 @@ time to disappear */
/* No comment provided by engineer. */
"pending approval" = "in attesa di approvazione";
+/* No comment provided by engineer. */
+"pending review" = "in attesa di revisione";
+
/* No comment provided by engineer. */
"Periodic" = "Periodicamente";
@@ -3866,6 +3943,9 @@ time to disappear */
/* token info */
"Please try to disable and re-enable notfications." = "Prova a disattivare e riattivare le notifiche.";
+/* snd group event chat item */
+"Please wait for group moderators to review your request to join the group." = "Attendi che i moderatori del gruppo revisionino la tua richiesta di entrare nel gruppo.";
+
/* token info */
"Please wait for token activation to complete." = "Attendi il completamento dell'attivazione del token.";
@@ -4146,6 +4226,9 @@ swipe action */
/* No comment provided by engineer. */
"Reject contact request" = "Rifiuta la richiesta di contatto";
+/* alert title */
+"Reject member?" = "Rifiutare il membro?";
+
/* No comment provided by engineer. */
"rejected" = "rifiutato";
@@ -4185,6 +4268,9 @@ swipe action */
/* profile update event chat item */
"removed contact address" = "indirizzo di contatto rimosso";
+/* No comment provided by engineer. */
+"removed from group" = "rimosso dal gruppo";
+
/* profile update event chat item */
"removed profile picture" = "immagine del profilo rimossa";
@@ -4233,6 +4319,9 @@ swipe action */
/* No comment provided by engineer. */
"Report reason?" = "Motivo della segnalazione?";
+/* alert title */
+"Report sent to moderators" = "Segnalazione inviata ai moderatori";
+
/* report reason */
"Report spam: only group moderators will see it." = "Segnala spam: solo i moderatori del gruppo lo vedranno.";
@@ -4248,6 +4337,9 @@ swipe action */
/* No comment provided by engineer. */
"Reports" = "Segnalazioni";
+/* No comment provided by engineer. */
+"request to join rejected" = "richiesta di entrare rifiutata";
+
/* chat list item title */
"requested to connect" = "richiesto di connettersi";
@@ -4302,9 +4394,21 @@ swipe action */
/* chat item action */
"Reveal" = "Rivela";
+/* No comment provided by engineer. */
+"review" = "revisiona";
+
/* No comment provided by engineer. */
"Review conditions" = "Leggi le condizioni";
+/* admission stage */
+"Review members" = "Revisiona i membri";
+
+/* admission stage description */
+"Review members before admitting (\"knocking\")." = "Revisiona i membri prima di ammetterli (\"bussare\").";
+
+/* No comment provided by engineer. */
+"reviewed by admins" = "revisionato dagli amministratori";
+
/* No comment provided by engineer. */
"Revoke" = "Revoca";
@@ -4333,6 +4437,9 @@ chat item action */
/* alert button */
"Save (and notify contacts)" = "Salva (e avvisa i contatti)";
+/* alert title */
+"Save admission settings?" = "Salvare le impostazioni di ammissione?";
+
/* alert button */
"Save and notify contact" = "Salva e avvisa il contatto";
@@ -4669,6 +4776,9 @@ chat item action */
/* No comment provided by engineer. */
"Set it instead of system authentication." = "Impostalo al posto dell'autenticazione di sistema.";
+/* No comment provided by engineer. */
+"Set member admission" = "Imposta l'ammissione del membro";
+
/* No comment provided by engineer. */
"Set message expiration in chats." = "Imposta la scadenza dei messaggi nelle chat.";
@@ -5100,9 +5210,6 @@ report reason */
/* No comment provided by engineer. */
"The old database was not removed during the migration, it can be deleted." = "Il database vecchio non è stato rimosso durante la migrazione, può essere eliminato.";
-/* No comment provided by engineer. */
-"Your profile is stored on your device and only shared with your contacts." = "Il profilo è condiviso solo con i tuoi contatti.";
-
/* No comment provided by engineer. */
"The same conditions will apply to operator **%@**." = "Le stesse condizioni si applicheranno all'operatore **%@**.";
@@ -5712,6 +5819,9 @@ report reason */
/* No comment provided by engineer. */
"You accepted connection" = "Hai accettato la connessione";
+/* snd group event chat item */
+"you accepted this member" = "hai accettato questo membro";
+
/* No comment provided by engineer. */
"You allow" = "Lo consenti";
@@ -5823,6 +5933,9 @@ report reason */
/* alert message */
"You can view invitation link again in connection details." = "Puoi vedere di nuovo il link di invito nei dettagli di connessione.";
+/* alert message */
+"You can view your reports in Chat with admins." = "Puoi vedere le tue segnalazioni nella chat con gli amministratori.";
+
/* No comment provided by engineer. */
"You can't send messages!" = "Non puoi inviare messaggi!";
@@ -5991,15 +6104,15 @@ report reason */
/* No comment provided by engineer. */
"Your profile **%@** will be shared." = "Verrà condiviso il tuo profilo **%@**.";
+/* No comment provided by engineer. */
+"Your profile is stored on your device and only shared with your contacts." = "Il profilo è condiviso solo con i tuoi contatti.";
+
/* No comment provided by engineer. */
"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti. I server di SimpleX non possono vedere il tuo profilo.";
/* alert message */
"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Il tuo profilo è stato cambiato. Se lo salvi, il profilo aggiornato verrà inviato a tutti i tuoi contatti.";
-/* No comment provided by engineer. */
-"Your profile, contacts and delivered messages are stored on your device." = "Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo.";
-
/* No comment provided by engineer. */
"Your random profile" = "Il tuo profilo casuale";
diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings
index d214f88e1c..481c21eb8b 100644
--- a/apps/ios/ja.lproj/Localizable.strings
+++ b/apps/ios/ja.lproj/Localizable.strings
@@ -1277,7 +1277,7 @@ swipe action */
/* No comment provided by engineer. */
"Don't enable" = "有効にしない";
-/* No comment provided by engineer. */
+/* alert action */
"Don't show again" = "次から表示しない";
/* No comment provided by engineer. */
@@ -1514,7 +1514,7 @@ swipe action */
/* alert title */
"Error receiving file" = "ファイル受信にエラー発生";
-/* No comment provided by engineer. */
+/* alert title */
"Error removing member" = "メンバー除名にエラー発生";
/* No comment provided by engineer. */
@@ -2295,6 +2295,7 @@ snd error text */
/* enabled status
group pref value
+member criteria value
time to disappear */
"off" = "オフ";
@@ -2307,7 +2308,8 @@ time to disappear */
/* feature offered item */
"offered %@: %@" = "提供された %1$@: %2$@";
-/* alert button */
+/* alert action
+alert button */
"Ok" = "OK";
/* No comment provided by engineer. */
@@ -3111,9 +3113,6 @@ chat item action */
/* No comment provided by engineer. */
"The old database was not removed during the migration, it can be deleted." = "古いデータベースは移行時に削除されなかったので、削除することができます。";
-/* No comment provided by engineer. */
-"Your profile is stored on your device and only shared with your contacts." = "プロフィールは連絡先にしか共有されません。";
-
/* No comment provided by engineer. */
"The second tick we missed! ✅" = "長らくお待たせしました! ✅";
@@ -3592,10 +3591,10 @@ chat item action */
"Your profile **%@** will be shared." = "あなたのプロファイル **%@** が共有されます。";
/* No comment provided by engineer. */
-"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "プロフィールはデバイスに保存され、連絡先とのみ共有されます。 SimpleX サーバーはあなたのプロファイルを参照できません。";
+"Your profile is stored on your device and only shared with your contacts." = "プロフィールは連絡先にしか共有されません。";
/* No comment provided by engineer. */
-"Your profile, contacts and delivered messages are stored on your device." = "あなたのプロフィール、連絡先、送信したメッセージがご自分の端末に保存されます。";
+"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "プロフィールはデバイスに保存され、連絡先とのみ共有されます。 SimpleX サーバーはあなたのプロファイルを参照できません。";
/* No comment provided by engineer. */
"Your random profile" = "あなたのランダム・プロフィール";
diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings
index 232de56641..5caea12ee2 100644
--- a/apps/ios/nl.lproj/Localizable.strings
+++ b/apps/ios/nl.lproj/Localizable.strings
@@ -1869,7 +1869,7 @@ swipe action */
/* No comment provided by engineer. */
"Don't miss important messages." = "Mis geen belangrijke berichten.";
-/* No comment provided by engineer. */
+/* alert action */
"Don't show again" = "Niet meer weergeven";
/* No comment provided by engineer. */
@@ -2233,7 +2233,7 @@ chat item action */
/* alert title */
"Error registering for notifications" = "Fout bij registreren voor meldingen";
-/* No comment provided by engineer. */
+/* alert title */
"Error removing member" = "Fout bij verwijderen van lid";
/* alert title */
@@ -3584,6 +3584,7 @@ snd error text */
/* enabled status
group pref value
+member criteria value
time to disappear */
"off" = "uit";
@@ -3596,7 +3597,8 @@ time to disappear */
/* feature offered item */
"offered %@: %@" = "voorgesteld %1$@: %2$@";
-/* alert button */
+/* alert action
+alert button */
"Ok" = "OK";
/* No comment provided by engineer. */
@@ -5091,9 +5093,6 @@ report reason */
/* No comment provided by engineer. */
"The old database was not removed during the migration, it can be deleted." = "De oude database is niet verwijderd tijdens de migratie, deze kan worden verwijderd.";
-/* No comment provided by engineer. */
-"Your profile is stored on your device and only shared with your contacts." = "Het profiel wordt alleen gedeeld met uw contacten.";
-
/* No comment provided by engineer. */
"The same conditions will apply to operator **%@**." = "Dezelfde voorwaarden gelden voor operator **%@**.";
@@ -5970,15 +5969,15 @@ report reason */
/* No comment provided by engineer. */
"Your profile **%@** will be shared." = "Uw profiel **%@** wordt gedeeld.";
+/* No comment provided by engineer. */
+"Your profile is stored on your device and only shared with your contacts." = "Het profiel wordt alleen gedeeld met uw contacten.";
+
/* No comment provided by engineer. */
"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten. SimpleX servers kunnen uw profiel niet zien.";
/* alert message */
"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Je profiel is gewijzigd. Als je het opslaat, wordt het bijgewerkte profiel naar al je contacten verzonden.";
-/* No comment provided by engineer. */
-"Your profile, contacts and delivered messages are stored on your device." = "Uw profiel, contacten en afgeleverde berichten worden op uw apparaat opgeslagen.";
-
/* No comment provided by engineer. */
"Your random profile" = "Je willekeurige profiel";
diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings
index 31a9b87662..e3e860e329 100644
--- a/apps/ios/pl.lproj/Localizable.strings
+++ b/apps/ios/pl.lproj/Localizable.strings
@@ -1761,7 +1761,7 @@ swipe action */
/* No comment provided by engineer. */
"Don't enable" = "Nie włączaj";
-/* No comment provided by engineer. */
+/* alert action */
"Don't show again" = "Nie pokazuj ponownie";
/* No comment provided by engineer. */
@@ -2092,7 +2092,7 @@ chat item action */
/* No comment provided by engineer. */
"Error reconnecting servers" = "Błąd ponownego łączenia serwerów";
-/* No comment provided by engineer. */
+/* alert title */
"Error removing member" = "Błąd usuwania członka";
/* No comment provided by engineer. */
@@ -3236,6 +3236,7 @@ snd error text */
/* enabled status
group pref value
+member criteria value
time to disappear */
"off" = "wyłączony";
@@ -3248,7 +3249,8 @@ time to disappear */
/* feature offered item */
"offered %@: %@" = "zaoferował %1$@: %2$@";
-/* alert button */
+/* alert action
+alert button */
"Ok" = "Ok";
/* No comment provided by engineer. */
@@ -4556,9 +4558,6 @@ chat item action */
/* No comment provided by engineer. */
"The old database was not removed during the migration, it can be deleted." = "Stara baza danych nie została usunięta podczas migracji, można ją usunąć.";
-/* No comment provided by engineer. */
-"Your profile is stored on your device and only shared with your contacts." = "Profil jest udostępniany tylko Twoim kontaktom.";
-
/* No comment provided by engineer. */
"The second tick we missed! ✅" = "Drugi tik, który przegapiliśmy! ✅";
@@ -5354,15 +5353,15 @@ chat item action */
/* No comment provided by engineer. */
"Your profile **%@** will be shared." = "Twój profil **%@** zostanie udostępniony.";
+/* No comment provided by engineer. */
+"Your profile is stored on your device and only shared with your contacts." = "Profil jest udostępniany tylko Twoim kontaktom.";
+
/* No comment provided by engineer. */
"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Twój profil jest przechowywany na urządzeniu i udostępniany tylko Twoim kontaktom. Serwery SimpleX nie mogą zobaczyć Twojego profilu.";
/* alert message */
"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Twój profil został zmieniony. Jeśli go zapiszesz, zaktualizowany profil zostanie wysłany do wszystkich kontaktów.";
-/* No comment provided by engineer. */
-"Your profile, contacts and delivered messages are stored on your device." = "Twój profil, kontakty i dostarczone wiadomości są przechowywane na Twoim urządzeniu.";
-
/* No comment provided by engineer. */
"Your random profile" = "Twój losowy profil";
diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings
index cb837836ff..759a4c79f4 100644
--- a/apps/ios/ru.lproj/Localizable.strings
+++ b/apps/ios/ru.lproj/Localizable.strings
@@ -164,7 +164,7 @@
"%d file(s) were not downloaded." = "%d файлов не было загружено.";
/* time interval */
-"%d hours" = "%d ч.";
+"%d hours" = "%d час.";
/* alert title */
"%d messages not forwarded" = "%d сообщений не переслано";
@@ -345,6 +345,12 @@ accept incoming call via notification
swipe action */
"Accept" = "Принять";
+/* alert action */
+"Accept as member" = "Принять в группу";
+
+/* alert action */
+"Accept as observer" = "Принять как читателя";
+
/* No comment provided by engineer. */
"Accept conditions" = "Принять условия";
@@ -358,6 +364,12 @@ swipe action */
swipe action */
"Accept incognito" = "Принять инкогнито";
+/* alert title */
+"Accept member" = "Принять члена";
+
+/* rcv group event chat item */
+"accepted %@" = "принят %@";
+
/* call status */
"accepted call" = "принятый звонок";
@@ -367,6 +379,9 @@ swipe action */
/* chat list item title */
"accepted invitation" = "принятое приглашение";
+/* rcv group event chat item */
+"accepted you" = "Вы приняты";
+
/* No comment provided by engineer. */
"Acknowledged" = "Подтверждено";
@@ -463,6 +478,9 @@ swipe action */
/* chat item text */
"agreeing encryption…" = "шифрование согласовывается…";
+/* member criteria value */
+"all" = "все";
+
/* No comment provided by engineer. */
"All" = "Все";
@@ -484,6 +502,9 @@ swipe action */
/* No comment provided by engineer. */
"All group members will remain connected." = "Все члены группы останутся соединены.";
+/* feature role */
+"all members" = "все члены";
+
/* No comment provided by engineer. */
"All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." = "Все сообщения и файлы отправляются с **end-to-end шифрованием**, с постквантовой безопасностью в прямых разговорах.";
@@ -502,6 +523,9 @@ swipe action */
/* No comment provided by engineer. */
"All reports will be archived for you." = "Все сообщения о нарушениях будут заархивированы для вас.";
+/* No comment provided by engineer. */
+"All servers" = "Все серверы";
+
/* No comment provided by engineer. */
"All your contacts will remain connected." = "Все контакты, которые соединились через этот адрес, сохранятся.";
@@ -899,6 +923,9 @@ marked deleted chat item preview text */
/* No comment provided by engineer. */
"Can't message member" = "Не удаётся отправить сообщение члену группы";
+/* No comment provided by engineer. */
+"can't send messages" = "нельзя отправлять";
+
/* alert action
alert button */
"Cancel" = "Отменить";
@@ -1036,9 +1063,18 @@ set passcode view */
/* No comment provided by engineer. */
"Chat will be deleted for you - this cannot be undone!" = "Разговор будет удален для Вас - это действие нельзя отменить!";
+/* chat toolbar */
+"Chat with admins" = "Чат с админами";
+
+/* No comment provided by engineer. */
+"Chat with member" = "Чат с членом группы";
+
/* No comment provided by engineer. */
"Chats" = "Чаты";
+/* No comment provided by engineer. */
+"Chats with members" = "Чаты с членами группы";
+
/* No comment provided by engineer. */
"Check messages every 20 min." = "Проверять сообщения каждые 20 минут.";
@@ -1327,9 +1363,15 @@ set passcode view */
/* No comment provided by engineer. */
"Contact already exists" = "Существующий контакт";
+/* No comment provided by engineer. */
+"contact deleted" = "контакт удален";
+
/* No comment provided by engineer. */
"Contact deleted!" = "Контакт удален!";
+/* No comment provided by engineer. */
+"contact disabled" = "контакт выключен";
+
/* No comment provided by engineer. */
"contact has e2e encryption" = "у контакта есть e2e шифрование";
@@ -1348,6 +1390,9 @@ set passcode view */
/* No comment provided by engineer. */
"Contact name" = "Имена контактов";
+/* No comment provided by engineer. */
+"contact not ready" = "контакт не готов";
+
/* No comment provided by engineer. */
"Contact preferences" = "Предпочтения контакта";
@@ -1596,6 +1641,9 @@ swipe action */
/* No comment provided by engineer. */
"Delete chat profile?" = "Удалить профиль?";
+/* alert title */
+"Delete chat with member?" = "Удалить чат с членом группы?";
+
/* No comment provided by engineer. */
"Delete chat?" = "Удалить разговор?";
@@ -1782,6 +1830,9 @@ swipe action */
/* No comment provided by engineer. */
"Direct messages between members are prohibited in this chat." = "Личные сообщения запрещены в этой группе.";
+/* No comment provided by engineer. */
+"Direct messages between members are prohibited." = "Прямые сообщения между членами запрещены.";
+
/* No comment provided by engineer. */
"Disable (keep overrides)" = "Выключить (кроме исключений)";
@@ -1836,6 +1887,9 @@ swipe action */
/* No comment provided by engineer. */
"Do it later" = "Отложить";
+/* No comment provided by engineer. */
+"Do not send history to new members." = "Не отправлять историю новым членам.";
+
/* No comment provided by engineer. */
"Do NOT send messages directly, even if your or destination server does not support private routing." = "Не отправлять сообщения напрямую, даже если сервер получателя не поддерживает конфиденциальную доставку.";
@@ -1860,7 +1914,7 @@ swipe action */
/* No comment provided by engineer. */
"Don't miss important messages." = "Не пропустите важные сообщения.";
-/* No comment provided by engineer. */
+/* alert action */
"Don't show again" = "Не показывать";
/* No comment provided by engineer. */
@@ -1933,6 +1987,9 @@ chat item action */
/* No comment provided by engineer. */
"Enable camera access" = "Включить доступ к камере";
+/* No comment provided by engineer. */
+"Enable Flux in Network & servers settings for better metadata privacy." = "Включите Flux в настройках Сеть и серверы для лучшей конфиденциальности метаданных.";
+
/* No comment provided by engineer. */
"Enable for all" = "Включить для всех";
@@ -2101,6 +2158,12 @@ chat item action */
/* No comment provided by engineer. */
"Error accepting contact request" = "Ошибка при принятии запроса на соединение";
+/* alert title */
+"Error accepting member" = "Ошибка вступления члена группы";
+
+/* No comment provided by engineer. */
+"Error adding member(s)" = "Ошибка при добавлении членов группы";
+
/* alert title */
"Error adding server" = "Ошибка добавления сервера";
@@ -2137,6 +2200,9 @@ chat item action */
/* alert title */
"Error creating list" = "Ошибка создания списка";
+/* No comment provided by engineer. */
+"Error creating member contact" = "Ошибка при создании контакта";
+
/* No comment provided by engineer. */
"Error creating message" = "Ошибка создания сообщения";
@@ -2152,6 +2218,9 @@ chat item action */
/* No comment provided by engineer. */
"Error deleting chat database" = "Ошибка при удалении данных чата";
+/* alert title */
+"Error deleting chat with member" = "Ошибка при удалении чата с членом группы";
+
/* No comment provided by engineer. */
"Error deleting chat!" = "Ошибка при удалении чата!";
@@ -2215,6 +2284,9 @@ chat item action */
/* alert title */
"Error registering for notifications" = "Ошибка регистрации для уведомлений";
+/* alert title */
+"Error removing member" = "Ошибка при удалении члена группы";
+
/* alert title */
"Error reordering lists" = "Ошибка сортировки списков";
@@ -2251,6 +2323,9 @@ chat item action */
/* No comment provided by engineer. */
"Error sending email" = "Ошибка отправки email";
+/* No comment provided by engineer. */
+"Error sending member contact invitation" = "Ошибка при отправке приглашения члену";
+
/* No comment provided by engineer. */
"Error sending message" = "Ошибка при отправке сообщения";
@@ -2451,6 +2526,9 @@ snd error text */
/* No comment provided by engineer. */
"Fix not supported by contact" = "Починка не поддерживается контактом";
+/* No comment provided by engineer. */
+"Fix not supported by group member" = "Починка не поддерживается членом группы.";
+
/* No comment provided by engineer. */
"For all moderators" = "Для всех модераторов";
@@ -2529,6 +2607,9 @@ snd error text */
/* No comment provided by engineer. */
"Full name (optional)" = "Полное имя (не обязательно)";
+/* No comment provided by engineer. */
+"Fully decentralized – visible only to members." = "Группа полностью децентрализована – она видна только членам.";
+
/* No comment provided by engineer. */
"Fully re-implemented - work in background!" = "Полностью обновлены - работают в фоне!";
@@ -2577,6 +2658,9 @@ snd error text */
/* No comment provided by engineer. */
"Group invitation is no longer valid, it was removed by sender." = "Приглашение в группу больше не действительно, оно было удалено отправителем.";
+/* No comment provided by engineer. */
+"group is deleted" = "группа удалена";
+
/* No comment provided by engineer. */
"Group link" = "Ссылка группы";
@@ -2595,12 +2679,18 @@ snd error text */
/* No comment provided by engineer. */
"Group profile" = "Профиль группы";
+/* No comment provided by engineer. */
+"Group profile is stored on members' devices, not on the servers." = "Профиль группы хранится на устройствах членов, а не на серверах.";
+
/* snd group event chat item */
"group profile updated" = "профиль группы обновлен";
/* No comment provided by engineer. */
"Group welcome message" = "Приветственное сообщение группы";
+/* No comment provided by engineer. */
+"Group will be deleted for all members - this cannot be undone!" = "Группа будет удалена для всех членов - это действие нельзя отменить!";
+
/* No comment provided by engineer. */
"Group will be deleted for you - this cannot be undone!" = "Группа будет удалена для Вас - это действие нельзя отменить!";
@@ -2637,6 +2727,9 @@ snd error text */
/* No comment provided by engineer. */
"History" = "История";
+/* No comment provided by engineer. */
+"History is not sent to new members." = "История не отправляется новым членам.";
+
/* time unit */
"hours" = "часов";
@@ -2871,6 +2964,9 @@ snd error text */
/* No comment provided by engineer. */
"Invite friends" = "Пригласить друзей";
+/* No comment provided by engineer. */
+"Invite members" = "Пригласить членов группы";
+
/* No comment provided by engineer. */
"Invite to chat" = "Пригласить в разговор";
@@ -3084,15 +3180,75 @@ snd error text */
/* blur media */
"Medium" = "Среднее";
+/* member role */
+"member" = "член группы";
+
+/* No comment provided by engineer. */
+"Member" = "Член группы";
+
+/* profile update event chat item */
+"member %@ changed to %@" = "член %1$@ изменился на %2$@";
+
+/* No comment provided by engineer. */
+"Member admission" = "Приём членов в группу";
+
/* rcv group event chat item */
"member connected" = "соединен(а)";
+/* No comment provided by engineer. */
+"member has old version" = "член имеет старую версию";
+
+/* item status text */
+"Member inactive" = "Член неактивен";
+
/* chat feature */
"Member reports" = "Сообщения о нарушениях";
/* No comment provided by engineer. */
"Member role will be changed to \"%@\". All chat members will be notified." = "Роль участника будет изменена на \"%@\". Все участники разговора получат уведомление.";
+/* No comment provided by engineer. */
+"Member role will be changed to \"%@\". All group members will be notified." = "Роль члена будет изменена на \"%@\". Все члены группы получат уведомление.";
+
+/* No comment provided by engineer. */
+"Member role will be changed to \"%@\". The member will receive a new invitation." = "Роль члена будет изменена на \"%@\". Будет отправлено новое приглашение.";
+
+/* No comment provided by engineer. */
+"Member will be removed from chat - this cannot be undone!" = "Член будет удален из разговора - это действие нельзя отменить!";
+
+/* No comment provided by engineer. */
+"Member will be removed from group - this cannot be undone!" = "Член группы будет удален - это действие нельзя отменить!";
+
+/* alert message */
+"Member will join the group, accept member?" = "Участник хочет присоединиться к группе. Принять?";
+
+/* No comment provided by engineer. */
+"Members can add message reactions." = "Члены могут добавлять реакции на сообщения.";
+
+/* No comment provided by engineer. */
+"Members can irreversibly delete sent messages. (24 hours)" = "Члены могут необратимо удалять отправленные сообщения. (24 часа)";
+
+/* No comment provided by engineer. */
+"Members can report messsages to moderators." = "Члены группы могут пожаловаться модераторам.";
+
+/* No comment provided by engineer. */
+"Members can send direct messages." = "Члены могут посылать прямые сообщения.";
+
+/* No comment provided by engineer. */
+"Members can send disappearing messages." = "Члены могут посылать исчезающие сообщения.";
+
+/* No comment provided by engineer. */
+"Members can send files and media." = "Члены могут слать файлы и медиа.";
+
+/* No comment provided by engineer. */
+"Members can send SimpleX links." = "Члены могут отправлять ссылки SimpleX.";
+
+/* No comment provided by engineer. */
+"Members can send voice messages." = "Члены могут отправлять голосовые сообщения.";
+
+/* No comment provided by engineer. */
+"Mention members 👋" = "Упоминайте участников 👋";
+
/* No comment provided by engineer. */
"Menus" = "Меню";
@@ -3114,6 +3270,9 @@ snd error text */
/* item status text */
"Message forwarded" = "Сообщение переслано";
+/* item status description */
+"Message may be delivered later if member becomes active." = "Сообщение может быть доставлено позже, если член группы станет активным.";
+
/* No comment provided by engineer. */
"Message queue info" = "Информация об очереди сообщений";
@@ -3330,6 +3489,12 @@ snd error text */
/* No comment provided by engineer. */
"New media options" = "Новые медиа-опции";
+/* No comment provided by engineer. */
+"New member role" = "Роль члена группы";
+
+/* rcv group event chat item */
+"New member wants to join the group." = "Новый участник хочет присоединиться к группе.";
+
/* notification */
"new message" = "новое сообщение";
@@ -3369,6 +3534,9 @@ snd error text */
/* No comment provided by engineer. */
"No chats in list %@" = "Нет чатов в списке %@";
+/* No comment provided by engineer. */
+"No chats with members" = "Нет чатов с членами группы";
+
/* No comment provided by engineer. */
"No contacts selected" = "Контакты не выбраны";
@@ -3453,6 +3621,9 @@ snd error text */
/* No comment provided by engineer. */
"Not compatible!" = "Несовместимая версия!";
+/* No comment provided by engineer. */
+"not synchronized" = "не синхронизирован";
+
/* No comment provided by engineer. */
"Notes" = "Заметки";
@@ -3477,11 +3648,15 @@ snd error text */
/* alert title */
"Notifications status" = "Статус уведомлений";
+/* No comment provided by engineer. */
+"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Теперь админы могут:\n- удалять сообщения членов.\n- приостанавливать членов (роль наблюдатель)";
+
/* member role */
"observer" = "читатель";
/* enabled status
group pref value
+member criteria value
time to disappear */
"off" = "нет";
@@ -3494,7 +3669,8 @@ time to disappear */
/* feature offered item */
"offered %@: %@" = "предложил(a) %1$@: %2$@";
-/* alert button */
+/* alert action
+alert button */
"Ok" = "Ок";
/* No comment provided by engineer. */
@@ -3590,6 +3766,9 @@ time to disappear */
/* No comment provided by engineer. */
"Open group" = "Открыть группу";
+/* alert title */
+"Open link?" = "Открыть ссылку?";
+
/* authentication reason */
"Open migration to another device" = "Открытие миграции на другое устройство";
@@ -3665,6 +3844,9 @@ time to disappear */
/* No comment provided by engineer. */
"Password to show" = "Пароль чтобы раскрыть";
+/* past/unknown group member */
+"Past member %@" = "Бывший член %@";
+
/* No comment provided by engineer. */
"Paste desktop address" = "Вставить адрес компьютера";
@@ -3689,6 +3871,9 @@ time to disappear */
/* No comment provided by engineer. */
"pending approval" = "ожидает утверждения";
+/* No comment provided by engineer. */
+"pending review" = "ожидает одобрения";
+
/* No comment provided by engineer. */
"Periodic" = "Периодически";
@@ -3758,6 +3943,9 @@ time to disappear */
/* token info */
"Please try to disable and re-enable notfications." = "Попробуйте выключить и снова включить уведомления.";
+/* snd group event chat item */
+"Please wait for group moderators to review your request to join the group." = "Пожалуйста, подождите, пока модераторы группы рассмотрят ваш запрос на вступление.";
+
/* token info */
"Please wait for token activation to complete." = "Пожалуйста, дождитесь завершения активации токена.";
@@ -3857,6 +4045,9 @@ time to disappear */
/* No comment provided by engineer. */
"Prohibit reporting messages to moderators." = "Запретить жаловаться модераторам группы.";
+/* No comment provided by engineer. */
+"Prohibit sending direct messages to members." = "Запретить посылать прямые сообщения членам группы.";
+
/* No comment provided by engineer. */
"Prohibit sending disappearing messages." = "Запретить посылать исчезающие сообщения.";
@@ -4035,6 +4226,9 @@ swipe action */
/* No comment provided by engineer. */
"Reject contact request" = "Отклонить запрос";
+/* alert title */
+"Reject member?" = "Отклонить участника?";
+
/* No comment provided by engineer. */
"rejected" = "отклонён";
@@ -4056,6 +4250,12 @@ swipe action */
/* No comment provided by engineer. */
"Remove image" = "Удалить изображение";
+/* No comment provided by engineer. */
+"Remove member" = "Удалить члена группы";
+
+/* No comment provided by engineer. */
+"Remove member?" = "Удалить члена группы?";
+
/* No comment provided by engineer. */
"Remove passphrase from keychain?" = "Удалить пароль из Keychain?";
@@ -4068,6 +4268,9 @@ swipe action */
/* profile update event chat item */
"removed contact address" = "удалён адрес контакта";
+/* No comment provided by engineer. */
+"removed from group" = "удален из группы";
+
/* profile update event chat item */
"removed profile picture" = "удалена картинка профиля";
@@ -4116,6 +4319,9 @@ swipe action */
/* No comment provided by engineer. */
"Report reason?" = "Причина сообщения?";
+/* alert title */
+"Report sent to moderators" = "Жалоба отправлена модераторам";
+
/* report reason */
"Report spam: only group moderators will see it." = "Пожаловаться на спам: увидят только модераторы группы.";
@@ -4131,6 +4337,9 @@ swipe action */
/* No comment provided by engineer. */
"Reports" = "Сообщения о нарушениях";
+/* No comment provided by engineer. */
+"request to join rejected" = "запрос на вступление отклонён";
+
/* chat list item title */
"requested to connect" = "запрошено соединение";
@@ -4185,9 +4394,21 @@ swipe action */
/* chat item action */
"Reveal" = "Показать";
+/* No comment provided by engineer. */
+"review" = "рассмотрение";
+
/* No comment provided by engineer. */
"Review conditions" = "Посмотреть условия";
+/* admission stage */
+"Review members" = "Одобрять членов";
+
+/* admission stage description */
+"Review members before admitting (\"knocking\")." = "Одобрять членов для вступления в группу.";
+
+/* No comment provided by engineer. */
+"reviewed by admins" = "одобрен админами";
+
/* No comment provided by engineer. */
"Revoke" = "Отозвать";
@@ -4216,9 +4437,15 @@ chat item action */
/* alert button */
"Save (and notify contacts)" = "Сохранить (и уведомить контакты)";
+/* alert title */
+"Save admission settings?" = "Сохранить настройки вступления?";
+
/* alert button */
"Save and notify contact" = "Сохранить и уведомить контакт";
+/* No comment provided by engineer. */
+"Save and notify group members" = "Сохранить и уведомить членов группы";
+
/* No comment provided by engineer. */
"Save and reconnect" = "Сохранить и переподключиться";
@@ -4411,6 +4638,9 @@ chat item action */
/* No comment provided by engineer. */
"Send them from gallery or custom keyboards." = "Отправьте из галереи или из дополнительных клавиатур.";
+/* No comment provided by engineer. */
+"Send up to 100 last messages to new members." = "Отправить до 100 последних сообщений новым членам.";
+
/* alert message */
"Sender cancelled file transfer." = "Отправитель отменил передачу файла.";
@@ -4546,6 +4776,9 @@ chat item action */
/* No comment provided by engineer. */
"Set it instead of system authentication." = "Установите код вместо системной аутентификации.";
+/* No comment provided by engineer. */
+"Set member admission" = "Приём членов в группу";
+
/* No comment provided by engineer. */
"Set message expiration in chats." = "Установите срок хранения сообщений в чатах.";
@@ -4564,6 +4797,9 @@ chat item action */
/* No comment provided by engineer. */
"Set passphrase to export" = "Установите пароль";
+/* No comment provided by engineer. */
+"Set the message shown to new members!" = "Установить сообщение для новых членов группы!";
+
/* No comment provided by engineer. */
"Set timeouts for proxy/VPN" = "Установить таймауты для прокси/VPN";
@@ -4616,6 +4852,9 @@ chat item action */
/* No comment provided by engineer. */
"Share with contacts" = "Поделиться с контактами";
+/* No comment provided by engineer. */
+"Short link" = "Короткая ссылка";
+
/* No comment provided by engineer. */
"Show → on messages sent via private routing." = "Показать → на сообщениях доставленных конфиденциально.";
@@ -4658,6 +4897,9 @@ chat item action */
/* No comment provided by engineer. */
"SimpleX address or 1-time link?" = "Адрес SimpleX или одноразовая ссылка?";
+/* simplex link type */
+"SimpleX channel link" = "SimpleX ссылка канала";
+
/* No comment provided by engineer. */
"SimpleX Chat and Flux made an agreement to include Flux-operated servers into the app." = "SimpleX Chat и Flux заключили соглашение добавить серверы под управлением Flux в приложение.";
@@ -4954,10 +5196,19 @@ report reason */
"The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "Неправильный ID предыдущего сообщения (меньше или равен предыдущему).\nЭто может произойти из-за ошибки программы, или когда соединение компроментировано.";
/* No comment provided by engineer. */
-"The old database was not removed during the migration, it can be deleted." = "Предыдущая версия данных чата не удалена при перемещении, её можно удалить.";
+"The message will be deleted for all members." = "Сообщение будет удалено для всех членов группы.";
/* No comment provided by engineer. */
-"Your profile is stored on your device and only shared with your contacts." = "Ваш профиль храниться на Вашем устройстве и отправляется только контактам.";
+"The message will be marked as moderated for all members." = "Сообщение будет помечено как удаленное для всех членов группы.";
+
+/* No comment provided by engineer. */
+"The messages will be deleted for all members." = "Сообщения будут удалены для всех членов группы.";
+
+/* No comment provided by engineer. */
+"The messages will be marked as moderated for all members." = "Сообщения будут помечены как удаленные для всех членов группы.";
+
+/* No comment provided by engineer. */
+"The old database was not removed during the migration, it can be deleted." = "Предыдущая версия данных чата не удалена при перемещении, её можно удалить.";
/* No comment provided by engineer. */
"The same conditions will apply to operator **%@**." = "Те же самые условия будут приняты для оператора **%@**.";
@@ -5022,6 +5273,9 @@ report reason */
/* No comment provided by engineer. */
"This display name is invalid. Please choose another name." = "Ошибка имени профиля. Пожалуйста, выберите другое имя.";
+/* No comment provided by engineer. */
+"This group has over %lld members, delivery receipts are not sent." = "В этой группе более %lld членов, отчёты о доставке не отправляются.";
+
/* No comment provided by engineer. */
"This group no longer exists." = "Эта группа больше не существует.";
@@ -5031,6 +5285,9 @@ report reason */
/* No comment provided by engineer. */
"This is your own SimpleX address!" = "Это ваш собственный адрес SimpleX!";
+/* No comment provided by engineer. */
+"This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link." = "Эта ссылка требует новую версию. Обновите приложение или попросите Ваш контакт прислать совместимую ссылку.";
+
/* No comment provided by engineer. */
"This link was used with another mobile device, please create a new link on the desktop." = "Эта ссылка была использована на другом мобильном, пожалуйста, создайте новую ссылку на компьютере.";
@@ -5142,6 +5399,15 @@ report reason */
/* No comment provided by engineer. */
"Unblock for all" = "Разблокировать для всех";
+/* No comment provided by engineer. */
+"Unblock member" = "Разблокировать члена группы";
+
+/* No comment provided by engineer. */
+"Unblock member for all?" = "Разблокировать члена для всех?";
+
+/* No comment provided by engineer. */
+"Unblock member?" = "Разблокировать члена группы?";
+
/* rcv group event chat item */
"unblocked %@" = "%@ разблокирован";
@@ -5214,6 +5480,12 @@ report reason */
/* swipe action */
"Unread" = "Не прочитано";
+/* No comment provided by engineer. */
+"Unsupported connection link" = "Ссылка не поддерживается";
+
+/* No comment provided by engineer. */
+"Up to 100 last messages are sent to new members." = "До 100 последних сообщений отправляются новым членам.";
+
/* No comment provided by engineer. */
"Update" = "Обновить";
@@ -5304,6 +5576,9 @@ report reason */
/* No comment provided by engineer. */
"Use servers" = "Использовать серверы";
+/* No comment provided by engineer. */
+"Use short links (BETA)" = "Короткие ссылки (БЕТА)";
+
/* No comment provided by engineer. */
"Use SimpleX Chat servers?" = "Использовать серверы предосталенные SimpleX Chat?";
@@ -5313,6 +5588,9 @@ report reason */
/* No comment provided by engineer. */
"Use TCP port %@ when no port is specified." = "Использовать TCP-порт %@, когда порт не указан.";
+/* No comment provided by engineer. */
+"Use TCP port 443 for preset servers only." = "Использовать TCP-порт 443 только для серверов по умолчанию.";
+
/* No comment provided by engineer. */
"Use the app while in the call." = "Используйте приложение во время звонка.";
@@ -5541,6 +5819,9 @@ report reason */
/* No comment provided by engineer. */
"You accepted connection" = "Вы приняли приглашение соединиться";
+/* snd group event chat item */
+"you accepted this member" = "Вы приняли этого члена";
+
/* No comment provided by engineer. */
"You allow" = "Вы разрешаете";
@@ -5631,6 +5912,9 @@ report reason */
/* No comment provided by engineer. */
"You can set lock screen notification preview via settings." = "Вы можете установить просмотр уведомлений на экране блокировки в настройках.";
+/* No comment provided by engineer. */
+"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились.";
+
/* No comment provided by engineer. */
"You can share this address with your contacts to let them connect with **%@**." = "Вы можете поделиться этим адресом с Вашими контактами, чтобы они могли соединиться с **%@**.";
@@ -5649,6 +5933,9 @@ report reason */
/* alert message */
"You can view invitation link again in connection details." = "Вы можете увидеть ссылку-приглашение снова открыв соединение.";
+/* alert message */
+"You can view your reports in Chat with admins." = "Вы можете найти Ваши жалобы в Чате с админами.";
+
/* No comment provided by engineer. */
"You can't send messages!" = "Вы не можете отправлять сообщения!";
@@ -5685,6 +5972,9 @@ report reason */
/* No comment provided by engineer. */
"You joined this group" = "Вы вступили в эту группу";
+/* No comment provided by engineer. */
+"You joined this group. Connecting to inviting group member." = "Вы вступили в эту группу. Устанавливается соединение с пригласившим членом группы.";
+
/* snd group event chat item */
"you left" = "Вы покинули группу";
@@ -5739,6 +6029,9 @@ report reason */
/* No comment provided by engineer. */
"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Вы будете аутентифицированы при запуске и возобновлении приложения, которое было 30 секунд в фоновом режиме.";
+/* No comment provided by engineer. */
+"You will connect to all group members." = "Вы соединитесь со всеми членами группы.";
+
/* No comment provided by engineer. */
"You will still receive calls and notifications from muted profiles when they are active." = "Вы все равно получите звонки и уведомления в профилях без звука, когда они активные.";
@@ -5811,15 +6104,15 @@ report reason */
/* No comment provided by engineer. */
"Your profile **%@** will be shared." = "Будет отправлен Ваш профиль **%@**.";
+/* No comment provided by engineer. */
+"Your profile is stored on your device and only shared with your contacts." = "Ваш профиль храниться на Вашем устройстве и отправляется только контактам.";
+
/* No comment provided by engineer. */
"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. SimpleX серверы не могут получить доступ к Вашему профилю.";
/* alert message */
"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Ваш профиль был изменен. Если вы сохраните его, обновленный профиль будет отправлен всем вашим контактам.";
-/* No comment provided by engineer. */
-"Your profile, contacts and delivered messages are stored on your device." = "Ваш профиль, контакты и доставленные сообщения хранятся на Вашем устройстве.";
-
/* No comment provided by engineer. */
"Your random profile" = "Случайный профиль";
@@ -5834,4 +6127,3 @@ report reason */
/* No comment provided by engineer. */
"Your SimpleX address" = "Ваш адрес SimpleX";
-
diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings
index 57c0466eb9..a6ef88d0d4 100644
--- a/apps/ios/th.lproj/Localizable.strings
+++ b/apps/ios/th.lproj/Localizable.strings
@@ -1028,7 +1028,7 @@ swipe action */
/* No comment provided by engineer. */
"Don't enable" = "อย่าเปิดใช้งาน";
-/* No comment provided by engineer. */
+/* alert action */
"Don't show again" = "ไม่ต้องแสดงอีก";
/* No comment provided by engineer. */
@@ -1256,7 +1256,7 @@ swipe action */
/* alert title */
"Error receiving file" = "เกิดข้อผิดพลาดในการรับไฟล์";
-/* No comment provided by engineer. */
+/* alert title */
"Error removing member" = "เกิดข้อผิดพลาดในการลบสมาชิก";
/* No comment provided by engineer. */
@@ -2016,6 +2016,7 @@ snd error text */
/* enabled status
group pref value
+member criteria value
time to disappear */
"off" = "ปิด";
@@ -2028,7 +2029,8 @@ time to disappear */
/* feature offered item */
"offered %@: %@" = "เสนอแล้ว %1$@: %2$@";
-/* alert button */
+/* alert action
+alert button */
"Ok" = "ตกลง";
/* No comment provided by engineer. */
@@ -2829,9 +2831,6 @@ chat item action */
/* No comment provided by engineer. */
"The old database was not removed during the migration, it can be deleted." = "ฐานข้อมูลเก่าไม่ได้ถูกลบในระหว่างการย้ายข้อมูล แต่สามารถลบได้";
-/* No comment provided by engineer. */
-"Your profile is stored on your device and only shared with your contacts." = "โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น";
-
/* No comment provided by engineer. */
"The second tick we missed! ✅" = "ขีดที่สองที่เราพลาด! ✅";
@@ -3292,10 +3291,10 @@ chat item action */
"Your privacy" = "ความเป็นส่วนตัวของคุณ";
/* No comment provided by engineer. */
-"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น เซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้";
+"Your profile is stored on your device and only shared with your contacts." = "โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น";
/* No comment provided by engineer. */
-"Your profile, contacts and delivered messages are stored on your device." = "โปรไฟล์ รายชื่อผู้ติดต่อ และข้อความที่ส่งของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณ";
+"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น เซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้";
/* No comment provided by engineer. */
"Your random profile" = "โปรไฟล์แบบสุ่มของคุณ";
diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings
index e3bb11d1cc..3d44c895ec 100644
--- a/apps/ios/tr.lproj/Localizable.strings
+++ b/apps/ios/tr.lproj/Localizable.strings
@@ -1740,7 +1740,7 @@ swipe action */
/* No comment provided by engineer. */
"Don't enable" = "Etkinleştirme";
-/* No comment provided by engineer. */
+/* alert action */
"Don't show again" = "Yeniden gösterme";
/* No comment provided by engineer. */
@@ -2083,7 +2083,7 @@ chat item action */
/* No comment provided by engineer. */
"Error reconnecting servers" = "Hata sunuculara yeniden bağlanılıyor";
-/* No comment provided by engineer. */
+/* alert title */
"Error removing member" = "Kişiyi silerken sorun oluştu";
/* No comment provided by engineer. */
@@ -3272,6 +3272,7 @@ snd error text */
/* enabled status
group pref value
+member criteria value
time to disappear */
"off" = "kapalı";
@@ -3284,7 +3285,8 @@ time to disappear */
/* feature offered item */
"offered %@: %@" = "%1$@: %2$@ teklif etti";
-/* alert button */
+/* alert action
+alert button */
"Ok" = "Tamam";
/* No comment provided by engineer. */
@@ -4601,9 +4603,6 @@ chat item action */
/* No comment provided by engineer. */
"The old database was not removed during the migration, it can be deleted." = "Eski veritabanı geçiş sırasında kaldırılmadı, silinebilir.";
-/* No comment provided by engineer. */
-"Your profile is stored on your device and only shared with your contacts." = "Profil sadece kişilerinle paylaşılacak.";
-
/* No comment provided by engineer. */
"The second tick we missed! ✅" = "Özlediğimiz ikinci tik! ✅";
@@ -5399,15 +5398,15 @@ chat item action */
/* No comment provided by engineer. */
"Your profile **%@** will be shared." = "Profiliniz **%@** paylaşılacaktır.";
+/* No comment provided by engineer. */
+"Your profile is stored on your device and only shared with your contacts." = "Profil sadece kişilerinle paylaşılacak.";
+
/* No comment provided by engineer. */
"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Profiliniz cihazınızda saklanır ve sadece kişilerinizle paylaşılır. SimpleX sunucuları profilinizi göremez.";
/* alert message */
"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Profiliniz değiştirildi. Kaydederseniz, güncellenmiş profil tüm kişilerinize gönderilecektir.";
-/* No comment provided by engineer. */
-"Your profile, contacts and delivered messages are stored on your device." = "Profiliniz, kişileriniz ve gönderilmiş mesajlar cihazınızda saklanır.";
-
/* No comment provided by engineer. */
"Your random profile" = "Rasgele profiliniz";
diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings
index 734b8dda82..932c29d368 100644
--- a/apps/ios/uk.lproj/Localizable.strings
+++ b/apps/ios/uk.lproj/Localizable.strings
@@ -1746,7 +1746,7 @@ swipe action */
/* No comment provided by engineer. */
"Don't enable" = "Не вмикати";
-/* No comment provided by engineer. */
+/* alert action */
"Don't show again" = "Більше не показувати";
/* No comment provided by engineer. */
@@ -2089,7 +2089,7 @@ chat item action */
/* No comment provided by engineer. */
"Error reconnecting servers" = "Помилка перепідключення серверів";
-/* No comment provided by engineer. */
+/* alert title */
"Error removing member" = "Помилка видалення учасника";
/* No comment provided by engineer. */
@@ -3317,6 +3317,7 @@ snd error text */
/* enabled status
group pref value
+member criteria value
time to disappear */
"off" = "вимкнено";
@@ -3329,7 +3330,8 @@ time to disappear */
/* feature offered item */
"offered %@: %@" = "запропонував %1$@: %2$@";
-/* alert button */
+/* alert action
+alert button */
"Ok" = "Гаразд";
/* No comment provided by engineer. */
@@ -4721,9 +4723,6 @@ chat item action */
/* No comment provided by engineer. */
"The old database was not removed during the migration, it can be deleted." = "Стара база даних не була видалена під час міграції, її можна видалити.";
-/* No comment provided by engineer. */
-"Your profile is stored on your device and only shared with your contacts." = "Профіль доступний лише вашим контактам.";
-
/* No comment provided by engineer. */
"The same conditions will apply to operator **%@**." = "Такі ж умови діятимуть і для оператора **%@**.";
@@ -5579,15 +5578,15 @@ chat item action */
/* No comment provided by engineer. */
"Your profile **%@** will be shared." = "Ваш профіль **%@** буде опублікований.";
+/* No comment provided by engineer. */
+"Your profile is stored on your device and only shared with your contacts." = "Профіль доступний лише вашим контактам.";
+
/* No comment provided by engineer. */
"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "Ваш профіль зберігається на вашому пристрої і доступний лише вашим контактам. Сервери SimpleX не бачать ваш профіль.";
/* alert message */
"Your profile was changed. If you save it, the updated profile will be sent to all your contacts." = "Ваш профіль було змінено. Якщо ви збережете його, оновлений профіль буде надіслано всім вашим контактам.";
-/* No comment provided by engineer. */
-"Your profile, contacts and delivered messages are stored on your device." = "Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої.";
-
/* No comment provided by engineer. */
"Your random profile" = "Ваш випадковий профіль";
diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings
index e3f9669d9f..19d7c268d4 100644
--- a/apps/ios/zh-Hans.lproj/Localizable.strings
+++ b/apps/ios/zh-Hans.lproj/Localizable.strings
@@ -1866,7 +1866,7 @@ swipe action */
/* No comment provided by engineer. */
"Don't miss important messages." = "不错过重要消息。";
-/* No comment provided by engineer. */
+/* alert action */
"Don't show again" = "不再显示";
/* No comment provided by engineer. */
@@ -2227,7 +2227,7 @@ chat item action */
/* alert title */
"Error registering for notifications" = "注册消息推送出错";
-/* No comment provided by engineer. */
+/* alert title */
"Error removing member" = "删除成员错误";
/* alert title */
@@ -3575,6 +3575,7 @@ snd error text */
/* enabled status
group pref value
+member criteria value
time to disappear */
"off" = "关闭";
@@ -3587,7 +3588,8 @@ time to disappear */
/* feature offered item */
"offered %@: %@" = "已提供 %1$@:%2$@";
-/* alert button */
+/* alert action
+alert button */
"Ok" = "好的";
/* No comment provided by engineer. */
@@ -4922,9 +4924,6 @@ chat item action */
/* No comment provided by engineer. */
"The old database was not removed during the migration, it can be deleted." = "旧数据库在迁移过程中没有被移除,可以删除。";
-/* No comment provided by engineer. */
-"Your profile is stored on your device and only shared with your contacts." = "该资料仅与您的联系人共享。";
-
/* No comment provided by engineer. */
"The second tick we missed! ✅" = "我们错过的第二个\"√\"!✅";
@@ -5697,10 +5696,10 @@ chat item action */
"Your profile **%@** will be shared." = "您的个人资料 **%@** 将被共享。";
/* No comment provided by engineer. */
-"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "您的资料存储在您的设备上并仅与您的联系人共享。 SimpleX 服务器无法看到您的资料。";
+"Your profile is stored on your device and only shared with your contacts." = "该资料仅与您的联系人共享。";
/* No comment provided by engineer. */
-"Your profile, contacts and delivered messages are stored on your device." = "您的资料、联系人和发送的消息存储在您的设备上。";
+"Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." = "您的资料存储在您的设备上并仅与您的联系人共享。 SimpleX 服务器无法看到您的资料。";
/* No comment provided by engineer. */
"Your random profile" = "您的随机资料";
diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts
index e2927e4aaf..345a75b1e7 100644
--- a/apps/multiplatform/common/build.gradle.kts
+++ b/apps/multiplatform/common/build.gradle.kts
@@ -155,7 +155,6 @@ 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"]}")
}
}
diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt
index 54e3061d25..a09ca2792b 100644
--- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt
+++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt
@@ -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) {
- unreadBadge(userInfo.unreadCount, userInfo.user.showNtfs, false)
+ userUnreadBadge(userInfo.unreadCount, userInfo.user.showNtfs, false)
}
}
val user = userInfo.user
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt
index 61c20587bf..270b3a73b2 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt
@@ -295,14 +295,13 @@ object ChatModel {
}
}
- class ChatsContext(val contentTag: MsgContentTag?) {
+ class ChatsContext(val secondaryContextFilter: SecondaryContextFilter?) {
val chats = mutableStateOf(SnapshotStateList())
/** 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())
- val chatItemStatuses = mutableMapOf()
// set listener here that will be notified on every add/delete of a chat item
val chatState = ActiveChatState()
@@ -310,6 +309,26 @@ 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)
@@ -342,6 +361,8 @@ 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)
}
@@ -364,7 +385,7 @@ object ChatModel {
updateContact(rhId, updatedContact)
}
- suspend fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo))
+ suspend fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo, groupChatScope = null))
private suspend fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) {
if (hasChat(rhId, cInfo.id)) {
@@ -416,55 +437,64 @@ 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 previews
+ // update chat list
val i = getChatIndex(rhId, cInfo.id)
val chat: Chat
if (i >= 0) {
- 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
+ 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 {
- currentPreviewItem
+ cItem
}
- } else {
- cItem
}
- }
- else -> cItem
- }
- 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)
+ 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)
+ }
+ // pop chat
if (appPlatform.isDesktop && cItem.chatDir.sent) {
- reorderChat(chats[i], 0)
+ reorderChat(chatsContext.chats[i], 0)
} else {
popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = i)
}
} else {
- addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
+ if (cInfo.groupChatScope() == null) {
+ addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
+ } else {
+ addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = emptyList()))
+ }
}
+ // add to current scope
withContext(Dispatchers.Main) {
- // add to current chat
- if (chatId.value == cInfo.id) {
+ if (chatItemBelongsToScope(cInfo, cItem)) {
// 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) {
@@ -476,84 +506,101 @@ object ChatModel {
}
}
}
-
- suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean {
- // 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)
+
+ 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
}
}
- res = false
- } else {
- addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
- res = true
+ is SecondaryContextFilter.MsgContentTagContext ->
+ chatId.value == cInfo.id && cItem.isReport
}
- return withContext(Dispatchers.Main) {
- // update current chat
- if (chatId.value == cInfo.id) {
+
+ 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)
+ }
+ }
+ } else {
+ addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
+ itemAdded = true
+ }
+ }
+ // update current scope
+ withContext(Dispatchers.Main) {
+ if (chatItemBelongsToScope(cInfo, cItem)) {
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) {
- 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))
+ 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))
} else {
cItem
}
- addToChatItems(ci)
- true
+ items[itemIndex] = ci
+ } else {
+ addToChatItems(cItem)
+ itemAdded = true
}
- } else {
- res
}
}
+ return itemAdded
}
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null, atIndex: Int? = null) {
withContext(Dispatchers.Main) {
- if (chatId.value == cInfo.id) {
+ if (chatItemBelongsToScope(cInfo, cItem)) {
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) {
- 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))
+ // 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))
+ }
}
}
- // remove from current chat
- if (chatId.value == cInfo.id) {
+ // remove from current scope
+ if (chatItemBelongsToScope(cInfo, cItem)) {
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)
@@ -586,7 +633,7 @@ object ChatModel {
Log.d(TAG, "exiting removeMemberItems")
return
}
- val cInfo = ChatInfo.Group(groupInfo)
+ val cInfo = ChatInfo.Group(groupInfo, groupChatScope = null) // TODO [knocking] review
if (chatId.value == groupInfo.id) {
for (i in 0 until chatItems.value.size) {
val updatedItem = removedUpdatedItem(chatItems.value[i])
@@ -617,7 +664,6 @@ object ChatModel {
}
// clear current chat
if (chatId.value == cInfo.id) {
- chatItemStatuses.clear()
chatItems.clearAndNotify()
}
}
@@ -688,7 +734,7 @@ object ChatModel {
chats[chatIdx] = chat.copy(
chatStats = chat.chatStats.copy(unreadCount = unreadCount, unreadMentions = unreadMentions)
)
- updateChatTagReadNoContentTag(chats[chatIdx], wasUnread)
+ updateChatTagReadInPrimaryContext(chats[chatIdx], wasUnread)
}
}
}
@@ -729,9 +775,9 @@ object ChatModel {
return markedRead to mentionsMarkedRead
}
- private fun decreaseCounterInChatNoContentTag(rhId: Long?, chatId: ChatId) {
+ private fun decreaseCounterInPrimaryContext(rhId: Long?, chatId: ChatId) {
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
- if (contentTag != null) return
+ if (secondaryContextFilter != null) return
val chatIndex = getChatIndex(rhId, chatId)
if (chatIndex == -1) return
@@ -745,7 +791,7 @@ object ChatModel {
unreadCount = unreadCount,
)
)
- updateChatTagReadNoContentTag(chats[chatIndex], wasUnread)
+ updateChatTagReadInPrimaryContext(chats[chatIndex], wasUnread)
}
fun removeChat(rhId: Long?, id: String) {
@@ -814,16 +860,16 @@ object ChatModel {
}
fun increaseUnreadCounter(rhId: Long?, user: UserLike) {
- changeUnreadCounterNoContentTag(rhId, user, 1)
+ changeUnreadCounterInPrimaryContext(rhId, user, 1)
}
fun decreaseUnreadCounter(rhId: Long?, user: UserLike, by: Int = 1) {
- changeUnreadCounterNoContentTag(rhId, user, -by)
+ changeUnreadCounterInPrimaryContext(rhId, user, -by)
}
- private fun changeUnreadCounterNoContentTag(rhId: Long?, user: UserLike, by: Int) {
+ private fun changeUnreadCounterInPrimaryContext(rhId: Long?, user: UserLike, by: Int) {
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
- if (contentTag != null) return
+ if (secondaryContextFilter != null) return
val i = users.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == rhId }
if (i != -1) {
@@ -831,9 +877,9 @@ object ChatModel {
}
}
- fun updateChatTagReadNoContentTag(chat: Chat, wasUnread: Boolean) {
+ fun updateChatTagReadInPrimaryContext(chat: Chat, wasUnread: Boolean) {
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
- if (contentTag != null) return
+ if (secondaryContextFilter != null) return
val tags = chat.chatInfo.chatTags ?: return
val nowUnread = chat.unreadTag
@@ -843,21 +889,21 @@ object ChatModel {
unreadTags[tag] = (unreadTags[tag] ?: 0) + 1
}
} else if (!nowUnread && wasUnread) {
- markChatTagReadNoContentTag_(chat, tags)
+ markChatTagReadInPrimaryContext_(chat, tags)
}
}
fun markChatTagRead(chat: Chat) {
if (chat.unreadTag) {
chat.chatInfo.chatTags?.let { tags ->
- markChatTagReadNoContentTag_(chat, tags)
+ markChatTagReadInPrimaryContext_(chat, tags)
}
}
}
- private fun markChatTagReadNoContentTag_(chat: Chat, tags: List) {
+ private fun markChatTagReadInPrimaryContext_(chat: Chat, tags: List) {
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
- if (contentTag != null) return
+ if (secondaryContextFilter != null) return
for (tag in tags) {
val count = unreadTags[tag]
@@ -889,12 +935,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
- changeGroupReportsTagNoContentTag(by)
+ changeGroupReportsTagInPrimaryContext(by)
}
}
- private fun changeGroupReportsTagNoContentTag(by: Int = 0) {
- if (by == 0 || contentTag != null) return
+ private fun changeGroupReportsTagInPrimaryContext(by: Int = 0) {
+ if (by == 0 || secondaryContextFilter != null) return
presetTags[PresetTagKind.GROUP_REPORTS] = kotlin.math.max(0, (presetTags[PresetTagKind.GROUP_REPORTS] ?: 0) + by)
clearActiveChatFilterIfNeeded()
}
@@ -1099,6 +1145,28 @@ 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?,
@@ -1204,8 +1272,6 @@ interface SomeChat {
val apiId: Long
val ready: Boolean
val chatDeleted: Boolean
- val userCantSendReason: Pair?
- val sendMsgEnabled: Boolean
val incognito: Boolean
fun featureEnabled(feature: ChatFeature): Boolean
val timedMessagesTTL: Int?
@@ -1237,6 +1303,16 @@ 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)
@@ -1275,8 +1351,6 @@ 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)
override val timedMessagesTTL: Int? get() = contact.timedMessagesTTL
@@ -1294,15 +1368,13 @@ sealed class ChatInfo: SomeChat, NamedChat {
}
@Serializable @SerialName("group")
- data class Group(val groupInfo: GroupInfo): ChatInfo() {
+ data class Group(val groupInfo: GroupInfo, val groupChatScope: GroupChatScopeInfo?): 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)
override val timedMessagesTTL: Int? get() = groupInfo.timedMessagesTTL
@@ -1314,7 +1386,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val localAlias get() = groupInfo.localAlias
companion object {
- val sampleData = Group(GroupInfo.sampleData)
+ val sampleData = Group(GroupInfo.sampleData, groupChatScope = null)
}
}
@@ -1326,8 +1398,6 @@ 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)
override val timedMessagesTTL: Int? get() = noteFolder.timedMessagesTTL
@@ -1351,8 +1421,6 @@ 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)
override val timedMessagesTTL: Int? get() = contactRequest.timedMessagesTTL
@@ -1376,8 +1444,6 @@ 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)
override val timedMessagesTTL: Int? get() = contactConnection.timedMessagesTTL
@@ -1406,8 +1472,6 @@ 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
override val timedMessagesTTL: Int? get() = null
@@ -1422,6 +1486,71 @@ sealed class ChatInfo: SomeChat, NamedChat {
}
}
+ val userCantSendReason: Pair?
+ get() {
+ when (this) {
+ is Direct -> {
+ // TODO [short links] this will have additional statuses for pending contact requests before they are accepted
+ if (contact.nextSendGrpInv) return null
+ if (!contact.active) return generalGetString(MR.strings.cant_send_message_contact_deleted) to null
+ if (!contact.sndReady) return generalGetString(MR.strings.cant_send_message_contact_not_ready) to null
+ if (contact.activeConn?.connectionStats?.ratchetSyncSendProhibited == true) return generalGetString(MR.strings.cant_send_message_contact_not_synchronized) to null
+ if (contact.activeConn?.connDisabled == true) return generalGetString(MR.strings.cant_send_message_contact_disabled) to null
+ return null
+ }
+ is Group -> {
+ if (groupInfo.membership.memberActive) {
+ when (groupChatScope) {
+ null -> {
+ if (groupInfo.membership.memberPending) {
+ return generalGetString(MR.strings.reviewed_by_admins) to generalGetString(MR.strings.observer_cant_send_message_desc)
+ }
+ if (groupInfo.membership.memberRole == GroupMemberRole.Observer) {
+ return generalGetString(MR.strings.observer_cant_send_message_title) to generalGetString(MR.strings.observer_cant_send_message_desc)
+ }
+ return null
+ }
+ is GroupChatScopeInfo.MemberSupport ->
+ if (groupChatScope.groupMember_ != null) {
+ if (
+ groupChatScope.groupMember_.versionRange.maxVersion < GROUP_KNOCKING_VERSION
+ && !groupChatScope.groupMember_.memberPending
+ ) {
+ return generalGetString(MR.strings.cant_send_message_member_has_old_version) to null
+ }
+ return null
+ } else {
+ return null
+ }
+ }
+ } else {
+ return when (groupInfo.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
+ }
+ }
+ }
+ is Local ->
+ return null
+ is ContactRequest ->
+ return generalGetString(MR.strings.cant_send_message_generic) to null
+ is ContactConnection ->
+ return generalGetString(MR.strings.cant_send_message_generic) to null
+ is InvalidJSON ->
+ return generalGetString(MR.strings.cant_send_message_generic) to null
+ }
+ }
+
+ val sendMsgEnabled get() = userCantSendReason == null
+
+ fun groupChatScope(): GroupChatScope? = when (this) {
+ is Group -> groupChatScope?.toChatScope()
+ else -> null
+ }
+
fun ntfsEnabled(ci: ChatItem): Boolean =
ntfsEnabled(ci.meta.userMention)
@@ -1465,7 +1594,13 @@ 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 {
@@ -1519,17 +1654,6 @@ 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 userCantSendReason: Pair?
- 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) {
@@ -1754,6 +1878,7 @@ data class GroupInfo (
override val updatedAt: Instant,
val chatTs: Instant?,
val uiThemes: ThemeModeOverrides? = null,
+ val membersRequireAttention: Int,
val chatTags: List,
val chatItemTTL: Long?,
override val localAlias: String,
@@ -1763,23 +1888,6 @@ data class GroupInfo (
override val apiId get() = groupId
override val ready get() = membership.memberActive
override val chatDeleted get() = false
- override val userCantSendReason: Pair? 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
@@ -1797,7 +1905,7 @@ data class GroupInfo (
get() = membership.memberRole == GroupMemberRole.Owner && membership.memberCurrent
val canDelete: Boolean
- get() = membership.memberRole == GroupMemberRole.Owner || !membership.memberCurrent
+ get() = membership.memberRole == GroupMemberRole.Owner || !membership.memberCurrentOrPending
val canAddMembers: Boolean
get() = membership.memberRole >= GroupMemberRole.Admin && membership.memberActive
@@ -1832,6 +1940,7 @@ data class GroupInfo (
updatedAt = Clock.System.now(),
chatTs = Clock.System.now(),
uiThemes = null,
+ membersRequireAttention = 0,
chatTags = emptyList(),
localAlias = "",
chatItemTTL = null
@@ -1849,7 +1958,8 @@ data class GroupProfile (
val description: String? = null,
override val image: String? = null,
override val localAlias: String = "",
- val groupPreferences: GroupPreferences? = null
+ val groupPreferences: GroupPreferences? = null,
+ val memberAdmission: GroupMemberAdmission? = null
): NamedChat {
companion object {
val sampleData = GroupProfile(
@@ -1859,6 +1969,27 @@ 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,
@@ -1887,7 +2018,9 @@ data class GroupMember (
val memberProfile: LocalProfile,
val memberContactId: Long? = null,
val memberContactProfileId: Long,
- var activeConn: Connection? = null
+ var activeConn: Connection? = null,
+ val supportChat: GroupSupportChat? = null,
+ val memberChatVRange: VersionRange
): NamedChat {
val id: String get() = "#$groupId @$groupMemberId"
val ready get() = activeConn?.connStatus == ConnStatus.Ready
@@ -1946,6 +2079,7 @@ data class GroupMember (
GroupMemberStatus.MemUnknown -> false
GroupMemberStatus.MemInvited -> false
GroupMemberStatus.MemPendingApproval -> true
+ GroupMemberStatus.MemPendingReview -> true
GroupMemberStatus.MemIntroduced -> false
GroupMemberStatus.MemIntroInvited -> false
GroupMemberStatus.MemAccepted -> false
@@ -1963,6 +2097,7 @@ data class GroupMember (
GroupMemberStatus.MemUnknown -> false
GroupMemberStatus.MemInvited -> false
GroupMemberStatus.MemPendingApproval -> false
+ GroupMemberStatus.MemPendingReview -> false
GroupMemberStatus.MemIntroduced -> true
GroupMemberStatus.MemIntroInvited -> true
GroupMemberStatus.MemAccepted -> true
@@ -1972,6 +2107,15 @@ 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
@@ -1990,6 +2134,8 @@ data class GroupMember (
&& userRole >= GroupMemberRole.Moderator && userRole >= memberRole && groupInfo.membership.memberActive
}
+ val versionRange: VersionRange = activeConn?.peerChatVRange ?: memberChatVRange
+
val memberIncognito = memberProfile.profileId != memberContactProfileId
companion object {
@@ -2007,11 +2153,20 @@ data class GroupMember (
memberProfile = LocalProfile.sampleData,
memberContactId = 1,
memberContactProfileId = 1L,
- activeConn = Connection.sampleData
+ activeConn = Connection.sampleData,
+ memberChatVRange = VersionRange(minVersion = 1, maxVersion = 15)
)
}
}
+@Serializable
+class GroupSupportChat (
+ val chatTs: Instant,
+ val unread: Int,
+ val memberAttention: Int,
+ val mentions: Int
+)
+
@Serializable
data class GroupMemberSettings(val showMessages: Boolean) {}
@@ -2068,6 +2223,7 @@ 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,
@@ -2084,6 +2240,7 @@ 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)
@@ -2101,6 +2258,7 @@ 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)
@@ -2155,8 +2313,6 @@ class NoteFolder(
override val apiId get() = noteFolderId
override val chatDeleted get() = false
override val ready get() = true
- override val userCantSendReason: Pair? = null
- override val sendMsgEnabled get() = true
override val incognito get() = false
override fun featureEnabled(feature: ChatFeature) = feature == ChatFeature.Voice
override val timedMessagesTTL: Int? get() = null
@@ -2192,8 +2348,6 @@ 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
override val timedMessagesTTL: Int? get() = null
@@ -2232,8 +2386,6 @@ 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
override val timedMessagesTTL: Int? get() = null
@@ -2451,11 +2603,16 @@ 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 -> null
+ is RcvGroupEvent.UserRole,
+ is RcvGroupEvent.UserDeleted,
+ is RcvGroupEvent.GroupDeleted,
+ is RcvGroupEvent.MemberCreatedContact,
+ is RcvGroupEvent.NewMemberPendingReview ->
+ null
else -> CIMergeCategory.RcvGroupEvent
}
is CIContent.SndGroupEventContent -> when (content.sndGroupEvent) {
- is SndGroupEvent.UserRole, is SndGroupEvent.UserLeft -> null
+ is SndGroupEvent.UserRole, is SndGroupEvent.UserLeft, is SndGroupEvent.MemberAccepted, is SndGroupEvent.UserPendingReview -> null
else -> CIMergeCategory.SndGroupEvent
}
else -> {
@@ -2526,6 +2683,8 @@ 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
@@ -2537,6 +2696,7 @@ 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
@@ -2718,6 +2878,11 @@ data class ChatItem (
}
}
+sealed class SecondaryContextFilter {
+ class GroupChatScopeContext(val groupScopeInfo: GroupChatScopeInfo): SecondaryContextFilter()
+ class MsgContentTagContext(val contentTag: MsgContentTag): SecondaryContextFilter()
+}
+
fun MutableState>.add(index: Int, elem: Chat) {
value = SnapshotStateList().apply { addAll(value); add(index, elem) }
}
@@ -2976,6 +3141,19 @@ 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,
@@ -3015,6 +3193,13 @@ 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()
@@ -4153,6 +4338,8 @@ 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()
@@ -4164,10 +4351,13 @@ 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) {
@@ -4183,6 +4373,7 @@ 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 =
@@ -4207,6 +4398,8 @@ 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)
@@ -4219,6 +4412,8 @@ 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)
}
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
index 979d79c839..7cb2d9fe5e 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
@@ -58,6 +58,9 @@ typealias ChatCtrl = Long
// version range that supports establishing direct connection with a group member (xGrpDirectInvVRange in core)
val CREATE_MEMBER_CONTACT_VERSION = 2
+// support group knocking (MsgScope)
+val GROUP_KNOCKING_VERSION = 15
+
enum class CallOnLockScreen {
DISABLE,
SHOW,
@@ -163,6 +166,7 @@ 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, "")
@@ -245,6 +249,7 @@ class AppPreferences {
liveMessageAlertShown to false,
showHiddenProfilesNotice to true,
showMuteProfileAlert to true,
+ showReportsInSupportChatAlert to true,
showDeleteConversationNotice to true,
showDeleteContactNotice to true,
)
@@ -415,6 +420,7 @@ 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"
@@ -882,8 +888,8 @@ object ChatController {
return null
}
- suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, contentTag: MsgContentTag? = null, pagination: ChatPagination, search: String = ""): Pair? {
- val r = sendCmd(rh, CC.ApiGetChat(type, id, contentTag, pagination, search))
+ suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, contentTag: MsgContentTag? = null, pagination: ChatPagination, search: String = ""): Pair? {
+ val r = sendCmd(rh, CC.ApiGetChat(type, id, scope, 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
@@ -917,8 +923,8 @@ object ChatController {
suspend fun apiReorderChatTags(rh: Long?, tagIds: List) = sendCommandOkResp(rh, CC.ApiReorderChatTags(tagIds))
- suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, live: Boolean = false, ttl: Int? = null, composedMessages: List): List? {
- val cmd = CC.ApiSendMessages(type, id, live, ttl, composedMessages)
+ suspend fun apiSendMessages(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, live: Boolean = false, ttl: Int? = null, composedMessages: List): List? {
+ val cmd = CC.ApiSendMessages(type, id, scope, live, ttl, composedMessages)
return processSendMessageCmd(rh, cmd)
}
@@ -969,27 +975,27 @@ object ChatController {
return null
}
- suspend fun apiGetChatItemInfo(rh: Long?, type: ChatType, id: Long, itemId: Long): ChatItemInfo? {
- val r = sendCmd(rh, CC.ApiGetChatItemInfo(type, id, itemId))
+ suspend fun apiGetChatItemInfo(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemId: Long): ChatItemInfo? {
+ val r = sendCmd(rh, CC.ApiGetChatItemInfo(type, id, scope, 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, fromChatType: ChatType, fromChatId: Long, itemIds: List, ttl: Int?): List? {
- val cmd = CC.ApiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl)
+ suspend fun apiForwardChatItems(rh: Long?, toChatType: ChatType, toChatId: Long, toScope: GroupChatScope?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, itemIds: List, ttl: Int?): List? {
+ val cmd = CC.ApiForwardChatItems(toChatType, toChatId, toScope, fromChatType, fromChatId, fromScope, itemIds, ttl)
return processSendMessageCmd(rh, cmd)?.map { it.chatItem }
}
- suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, chatItemIds: List): CR.ForwardPlan? {
- val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, chatItemIds))
+ suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, fromScope: GroupChatScope?, chatItemIds: List): CR.ForwardPlan? {
+ val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, fromScope, 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, itemId: Long, updatedMessage: UpdatedMessage, live: Boolean = false): AChatItem? {
- val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, itemId, updatedMessage, live))
+ 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))
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
@@ -1011,8 +1017,8 @@ object ChatController {
return null
}
- 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))
+ 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))
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
@@ -1026,8 +1032,8 @@ object ChatController {
return null
}
- suspend fun apiDeleteChatItems(rh: Long?, type: ChatType, id: Long, itemIds: List, mode: CIDeleteMode): List? {
- val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, itemIds, mode))
+ suspend fun apiDeleteChatItems(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemIds: List, mode: CIDeleteMode): List? {
+ val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, scope, 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
@@ -1659,18 +1665,18 @@ object ChatController {
return null
}
- suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long): Boolean {
- val r = sendCmd(rh, CC.ApiChatRead(type, id))
+ suspend fun apiChatRead(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?): Boolean {
+ val r = sendCmd(rh, CC.ApiChatRead(type, id, scope))
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, itemIds: List): Boolean {
- val r = sendCmd(rh, CC.ApiChatItemsRead(type, id, itemIds))
- if (r.result is CR.CmdOk) return true
+ suspend fun apiChatItemsRead(rh: Long?, type: ChatType, id: Long, scope: GroupChatScope?, itemIds: List): 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
Log.e(TAG, "apiChatItemsRead bad response: ${r.responseType} ${r.details}")
- return false
+ return null
}
suspend fun apiChatUnread(rh: Long?, type: ChatType, id: Long, unreadChat: Boolean): Boolean {
@@ -1899,9 +1905,25 @@ object ChatController {
}
}
- suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List, withMessages: Boolean = false): List? {
+ suspend fun apiAcceptMember(rh: Long?, groupId: Long, groupMemberId: Long, memberRole: GroupMemberRole): Pair? {
+ 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 apiDeleteMemberSupportChat(rh: Long?, groupId: Long, groupMemberId: Long): Pair? {
+ val r = sendCmd(rh, CC.ApiDeleteMemberSupportChat(groupId, groupMemberId))
+ if (r is API.Result && r.res is CR.MemberSupportChatDeleted) return r.res.groupInfo to r.res.member
+ apiErrorAlert("apiDeleteMemberSupportChat", generalGetString(MR.strings.error_deleting_member_support_chat), r)
+ return null
+ }
+
+ suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List, withMessages: Boolean = false): Pair>? {
val r = sendCmd(rh, CC.ApiRemoveMembers(groupId, memberIds, withMessages))
- if (r is API.Result && r.res is CR.UserDeletedMembers) return r.res.members
+ if (r is API.Result && r.res is CR.UserDeletedMembers) return r.res.groupInfo to r.res.members
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiRemoveMembers", generalGetString(MR.strings.error_removing_member), r)
}
@@ -2420,9 +2442,7 @@ object ChatController {
}
}
withContext(Dispatchers.Main) {
- if (cItem.isReport) {
- chatModel.secondaryChatsContext.value?.addChatItem(rhId, cInfo, cItem)
- }
+ chatModel.secondaryChatsContext.value?.addChatItem(rhId, cInfo, cItem)
}
} else if (cItem.isRcvNew && cInfo.ntfsEnabled(cItem)) {
withContext(Dispatchers.Main) {
@@ -2448,12 +2468,10 @@ object ChatController {
val cItem = chatItem.chatItem
if (!cItem.isDeletedContent && active(r.user)) {
withContext(Dispatchers.Main) {
- chatModel.chatsContext.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
+ chatModel.chatsContext.upsertChatItem(rhId, cInfo, cItem)
}
withContext(Dispatchers.Main) {
- if (cItem.isReport) {
- chatModel.secondaryChatsContext.value?.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
- }
+ chatModel.secondaryChatsContext.value?.upsertChatItem(rhId, cInfo, cItem)
}
}
}
@@ -2465,9 +2483,7 @@ object ChatController {
chatModel.chatsContext.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
}
withContext(Dispatchers.Main) {
- if (r.reaction.chatReaction.chatItem.isReport) {
- chatModel.secondaryChatsContext.value?.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
- }
+ chatModel.secondaryChatsContext.value?.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
}
}
}
@@ -2510,15 +2526,18 @@ object ChatController {
}
}
withContext(Dispatchers.Main) {
- if (cItem.isReport) {
- if (toChatItem == null) {
- chatModel.secondaryChatsContext.value?.removeChatItem(rhId, cInfo, cItem)
- } else {
- chatModel.secondaryChatsContext.value?.upsertChatItem(rhId, cInfo, toChatItem.chatItem)
- }
+ if (toChatItem == null) {
+ chatModel.secondaryChatsContext.value?.removeChatItem(rhId, cInfo, cItem)
+ } else {
+ chatModel.secondaryChatsContext.value?.upsertChatItem(rhId, cInfo, toChatItem.chatItem)
}
}
}
+ r.chatItemDeletions.lastOrNull()?.deletedChatItem?.chatInfo?.let { updatedChatInfo ->
+ withContext(Dispatchers.Main) {
+ chatModel.chatsContext.updateChatInfo(rhId, updatedChatInfo)
+ }
+ }
}
is CR.GroupChatItemsDeleted -> {
groupChatItemsDeleted(rhId, r)
@@ -2575,6 +2594,13 @@ 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) {
@@ -2592,6 +2618,7 @@ 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)
@@ -2607,6 +2634,7 @@ 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) {
@@ -2655,6 +2683,16 @@ 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)) {
@@ -3016,11 +3054,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) {
- if (cItem.isReport) {
- chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem)
- }
+ chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem)
+ }
+ withContext(Dispatchers.Main) {
+ chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem)
}
}
}
@@ -3032,7 +3070,7 @@ object ChatController {
chatModel.users.addAll(users)
return
}
- val cInfo = ChatInfo.Group(r.groupInfo)
+ val cInfo = ChatInfo.Group(r.groupInfo, groupChatScope = null) // TODO [knocking] get scope from items?
withContext(Dispatchers.Main) {
val chatsCtx = chatModel.chatsContext
r.chatItemIDs.forEach { itemId ->
@@ -3087,11 +3125,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) {
- if (cItem.content.msgContent is MsgContent.MCReport) {
- chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem)
- }
+ chatModel.secondaryChatsContext.value?.upsertChatItem(rh, cInfo, cItem)
}
if (createdChat) {
notify()
@@ -3296,9 +3334,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 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): 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): CC()
class ApiCreateChatTag(val tag: ChatTagData): CC()
class ApiSetChatTags(val type: ChatType, val id: Long, val tagIds: List): CC()
class ApiDeleteChatTag(val tagId: Long): CC()
@@ -3306,18 +3344,20 @@ sealed class CC {
class ApiReorderChatTags(val tagIds: List): CC()
class ApiCreateChatItems(val noteFolderId: Long, val composedMessages: List): 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 itemId: Long, val updatedMessage: UpdatedMessage, val live: Boolean): CC()
- class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemIds: List, val mode: CIDeleteMode): 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, val mode: CIDeleteMode): CC()
class ApiDeleteMemberChatItem(val groupId: Long, val itemIds: List): CC()
class ApiArchiveReceivedReports(val groupId: Long): CC()
class ApiDeleteReceivedReports(val groupId: Long, val itemIds: List, val mode: CIDeleteMode): CC()
- class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
+ class ApiChatItemReaction(val type: ChatType, val id: Long, val scope: GroupChatScope?, 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 chatItemIds: List): CC()
- class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemIds: List, val ttl: Int?): CC()
+ class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val chatItemIds: List): CC()
+ class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val toScope: GroupChatScope?, val fromChatType: ChatType, val fromChatId: Long, val fromScope: GroupChatScope?, val itemIds: List, 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 ApiDeleteMemberSupportChat(val groupId: Long, val groupMemberId: Long): CC()
class ApiMembersRole(val groupId: Long, val memberIds: List, val memberRole: GroupMemberRole): CC()
class ApiBlockMembersForAll(val groupId: Long, val memberIds: List, val blocked: Boolean): CC()
class ApiRemoveMembers(val groupId: Long, val memberIds: List, val withMessages: Boolean): CC()
@@ -3395,8 +3435,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): CC()
- class ApiChatItemsRead(val type: ChatType, val id: Long, val itemIds: List): 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): 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()
@@ -3468,16 +3508,16 @@ sealed class CC {
} else {
" content=${contentTag.name.lowercase()}"
}
- "/_get chat ${chatRef(type, id)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
+ "/_get chat ${chatRef(type, id, scope)}$tag ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
}
- is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
+ is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id, scope)} $itemId"
is ApiSendMessages -> {
val msgs = json.encodeToString(composedMessages)
val ttlStr = if (ttl != null) "$ttl" else "default"
- "/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json $msgs"
+ "/_send ${chatRef(type, id, scope)} live=${onOff(live)} ttl=${ttlStr} json $msgs"
}
is ApiCreateChatTag -> "/_create tag ${json.encodeToString(tag)}"
- is ApiSetChatTags -> "/_tags ${chatRef(type, id)} ${tagIds.joinToString(",")}"
+ is ApiSetChatTags -> "/_tags ${chatRef(type, id, scope = null)} ${tagIds.joinToString(",")}"
is ApiDeleteChatTag -> "/_delete tag $tagId"
is ApiUpdateChatTag -> "/_update tag $tagId ${json.encodeToString(tagData)}"
is ApiReorderChatTags -> "/_reorder tags ${tagIds.joinToString(",")}"
@@ -3486,23 +3526,25 @@ 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)} $itemId live=${onOff(live)} ${updatedMessage.cmdString}"
- is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} ${itemIds.joinToString(",")} ${mode.deleteMode}"
+ 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 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)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}"
+ is ApiChatItemReaction -> "/_reaction ${chatRef(type, id, scope)} $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)} ${chatRef(fromChatType, fromChatId)} ${itemIds.joinToString(",")} ttl=${ttlStr}"
+ "/_forward ${chatRef(toChatType, toChatId, toScope)} ${chatRef(fromChatType, fromChatId, fromScope)} ${itemIds.joinToString(",")} ttl=${ttlStr}"
}
is ApiPlanForwardChatItems -> {
- "/_forward plan ${chatRef(fromChatType, fromChatId)} ${chatItemIds.joinToString(",")}"
+ "/_forward plan ${chatRef(fromChatType, fromChatId, fromScope)} ${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 ApiDeleteMemberSupportChat -> "/_delete member chat #$groupId $groupMemberId"
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)}"
@@ -3526,13 +3568,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)} ${chatItemTTLStr(seconds)}"
+ is APISetChatTTL -> "/_ttl $userId ${chatRef(chatType, id, scope = null)} ${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)} ${json.encodeToString(chatSettings)}"
+ is APISetChatSettings -> "/_settings ${chatRef(type, id, scope = null)} ${json.encodeToString(chatSettings)}"
is ApiSetMemberSettings -> "/_member settings #$groupId $groupMemberId ${json.encodeToString(memberSettings)}"
is APIContactInfo -> "/_info @$contactId"
is APIGroupMemberInfo -> "/_info #$groupId $groupMemberId"
@@ -3554,8 +3596,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)} ${chatDeleteMode.cmdString}"
- is ApiClearChat -> "/_clear chat ${chatRef(type, id)}"
+ is ApiDeleteChat -> "/_delete ${chatRef(type, id, scope = null)} ${chatDeleteMode.cmdString}"
+ is ApiClearChat -> "/_clear chat ${chatRef(type, id, scope = null)}"
is ApiListContacts -> "/_contacts $userId"
is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}"
is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}"
@@ -3580,9 +3622,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)}"
- is ApiChatItemsRead -> "/_read chat items ${chatRef(type, id)} ${itemIds.joinToString(",")}"
- is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
+ 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 ReceiveFile ->
"/freceive $fileId" +
" approved_relays=${onOff(userApprovedRelays)}" +
@@ -3666,6 +3708,8 @@ sealed class CC {
is ApiNewGroup -> "apiNewGroup"
is ApiAddMember -> "apiAddMember"
is ApiJoinGroup -> "apiJoinGroup"
+ is ApiAcceptMember -> "apiAcceptMember"
+ is ApiDeleteMemberSupportChat -> "apiDeleteMemberSupportChat"
is ApiMembersRole -> "apiMembersRole"
is ApiBlockMembersForAll -> "apiBlockMembersForAll"
is ApiRemoveMembers -> "apiRemoveMembers"
@@ -3802,7 +3846,13 @@ 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) = "${chatType.type}${id}"
+ 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_})"
+ }
+ }
}
}
@@ -5764,6 +5814,7 @@ 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()
@@ -5812,6 +5863,9 @@ 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("memberSupportChatDeleted") class MemberSupportChatDeleted(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, 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()
@@ -5942,6 +5996,7 @@ sealed class CR {
is ContactAlreadyExists -> "contactAlreadyExists"
is ContactDeleted -> "contactDeleted"
is ContactDeletedByContact -> "contactDeletedByContact"
+ is ItemsReadForChat -> "itemsReadForChat"
is ChatCleared -> "chatCleared"
is UserProfileNoChange -> "userProfileNoChange"
is UserProfileUpdated -> "userProfileUpdated"
@@ -5987,6 +6042,9 @@ sealed class CR {
is ReceivedGroupInvitation -> "receivedGroupInvitation"
is GroupDeletedUser -> "groupDeletedUser"
is JoinedGroupMemberConnecting -> "joinedGroupMemberConnecting"
+ is MemberAccepted -> "memberAccepted"
+ is MemberSupportChatDeleted -> "memberSupportChatDeleted"
+ is MemberAcceptedByOther -> "memberAcceptedByOther"
is MemberRole -> "memberRole"
is MembersRoleUser -> "membersRoleUser"
is MemberBlockedForAll -> "memberBlockedForAll"
@@ -6110,6 +6168,7 @@ 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))
@@ -6155,6 +6214,9 @@ 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 MemberSupportChatDeleted -> 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")
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt
index 2a77d0a6dc..a8b77e8fdd 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt
@@ -40,9 +40,8 @@ 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.ChatTTLSection
+import chat.simplex.common.views.chat.group.ChatTTLOption
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.*
@@ -617,7 +616,10 @@ fun ChatInfoLayout(
}
SectionDividerSpaced(maxBottomPadding = false)
- ChatTTLSection(chatItemTTL, setChatItemTTL, deletingItems)
+ SectionView {
+ ChatTTLOption(chatItemTTL, setChatItemTTL, deletingItems)
+ SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer))
+ }
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
val conn = contact.activeConn
@@ -1384,7 +1386,7 @@ private fun setChatTTL(
private suspend fun afterSetChatTTL(chatsCtx: ChatModel.ChatsContext, rhId: Long?, chatInfo: ChatInfo, progressIndicator: MutableState) {
try {
val pagination = ChatPagination.Initial(ChatPagination.INITIAL_COUNT)
- val (chat, navInfo) = controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, null, pagination) ?: return
+ val (chat, navInfo) = controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, scope = null, contentTag = 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) {
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt
index eabe9cb60a..07cdc065a7 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt
@@ -17,7 +17,7 @@ suspend fun apiLoadSingleMessage(
apiId: Long,
itemId: Long
): ChatItem? = coroutineScope {
- val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.contentTag, ChatPagination.Around(itemId, 0), "") ?: return@coroutineScope null
+ val (chat, _) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), 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.contentTag, pagination, search) ?: return@coroutineScope
+ val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, chatsCtx.groupScopeInfo?.toChatScope(), 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.contentTag == null) {
+ if (chatsCtx.secondaryContextFilter == null) {
// update main chats, not content tagged
withContext(Dispatchers.Main) {
val oldChat = chatModel.chatsContext.getChat(chat.id)
@@ -68,7 +68,6 @@ suspend fun processLoadedChat(
}
}
withContext(Dispatchers.Main) {
- chatsCtx.chatItemStatuses.clear()
chatsCtx.chatItems.replaceAll(chat.chatItems)
chatModel.chatId.value = chat.id
splits.value = newSplits
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt
index 6d7cdcdebe..2ca0dcc35d 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt
@@ -63,12 +63,23 @@ fun ChatView(
onComposed: suspend (chatId: String) -> Unit
) {
val showSearch = rememberSaveable { mutableStateOf(false) }
+ val chat = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }
// They have their own iterator inside for a reason to prevent crash "Reading a state that was created after the snapshot..."
val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } }
- val activeChatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo } }
+ val activeChatInfo = remember { derivedStateOf {
+ var chatInfo = chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo
+ if (
+ chatsCtx.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext
+ && chatInfo is ChatInfo.Group
+ ) {
+ val scopeInfo = chatsCtx.secondaryContextFilter.groupScopeInfo
+ chatInfo = chatInfo.copy(groupChatScope = scopeInfo)
+ }
+ chatInfo
+ } }
val user = chatModel.currentUser.value
val chatInfo = activeChatInfo.value
- if (chatInfo == null || user == null) {
+ if (chat == null || chatInfo == null || user == null) {
LaunchedEffect(Unit) {
chatModel.chatId.value = null
ModalManager.end.closeModals()
@@ -99,7 +110,7 @@ fun ChatView(
.distinctUntilChanged()
.filterNotNull()
.collect { chatId ->
- if (chatsCtx.contentTag == null) {
+ if (chatsCtx.secondaryContextFilter == null) {
markUnreadChatAsRead(chatId)
}
showSearch.value = false
@@ -108,6 +119,18 @@ fun ChatView(
}
}
}
+ if (chatsCtx.secondaryContextFilter == null && chatInfo is ChatInfo.Group && chatInfo.groupInfo.membership.memberPending) {
+ LaunchedEffect(Unit) {
+ val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = null)
+ val supportChatInfo = ChatInfo.Group(chatInfo.groupInfo, groupChatScope = scopeInfo)
+ showMemberSupportChatView(
+ chatModel.chatId,
+ scrollToItemId,
+ supportChatInfo,
+ scopeInfo
+ )
+ }
+ }
val view = LocalMultiplatformView()
val chatRh = remoteHostId.value
// We need to have real unreadCount value for displaying it inside top right button
@@ -117,6 +140,7 @@ fun ChatView(
chatsCtx.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats?.unreadCount ?: 0
}
}
+ val reportsCount = reportsCount(chatInfo.id)
val clipboard = LocalClipboardManager.current
CompositionLocalProvider(
LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false),
@@ -133,7 +157,7 @@ fun ChatView(
val sameText = searchText.value == value
// showSearch can be false with empty text when it was closed manually after clicking on message from search to load .around it
// (required on Android to have this check to prevent call to search with old text)
- val emptyAndClosedSearch = searchText.value.isEmpty() && !showSearch.value && chatsCtx.contentTag == null
+ val emptyAndClosedSearch = searchText.value.isEmpty() && !showSearch.value && chatsCtx.secondaryContextFilter == null
val c = chatModel.getChat(chatInfo.id)
if (sameText || emptyAndClosedSearch || c == null || chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged
withBGApi {
@@ -167,7 +191,7 @@ fun ChatView(
)
}
ComposeView(
- chatModel, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, attachmentOption,
+ rhId = remoteHostId.value, chatModel, chatsCtx, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, attachmentOption,
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } },
focusRequester = focusRequester
)
@@ -220,6 +244,7 @@ fun ChatView(
rh = chatRh,
fromChatType = chatInfo.chatType,
fromChatId = chatInfo.apiId,
+ fromScope = chatInfo.groupChatScope(),
chatItemIds = chatItemIds
)
@@ -317,7 +342,7 @@ fun ChatView(
}
}
},
- showGroupReports = {
+ showReports = {
val info = activeChatInfo.value ?: return@ChatLayout
if (ModalManager.end.hasModalsOpen()) {
ModalManager.end.closeModals()
@@ -328,6 +353,38 @@ fun ChatView(
showGroupReportsView(staleChatId, scrollToItemId, info)
}
},
+ showSupportChats = {
+ val info = activeChatInfo.value ?: return@ChatLayout
+ if (ModalManager.end.hasModalsOpen()) {
+ ModalManager.end.closeModals()
+ return@ChatLayout
+ }
+ hideKeyboard(view)
+ scope.launch {
+ if (info is ChatInfo.Group && info.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
+ ModalManager.end.showCustomModal { close ->
+ MemberSupportView(
+ chatRh,
+ chat,
+ info.groupInfo,
+ scrollToItemId,
+ close
+ )
+ }
+ } else if (info is ChatInfo.Group) {
+ val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = null)
+ val supportChatInfo = ChatInfo.Group(info.groupInfo, groupChatScope = scopeInfo)
+ scope.launch {
+ showMemberSupportChatView(
+ chatModel.chatId,
+ scrollToItemId = scrollToItemId,
+ supportChatInfo,
+ scopeInfo
+ )
+ }
+ }
+ }
+ },
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
hideKeyboard(view)
groupMembersJob.cancel()
@@ -343,12 +400,12 @@ fun ChatView(
setGroupMembers(chatRh, groupInfo, chatModel)
if (!isActive) return@launch
- if (chatsCtx.contentTag == null) {
+ if (chatsCtx.secondaryContextFilter == null) {
ModalManager.end.closeModals()
}
ModalManager.end.showModalCloseable(true) { close ->
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
- GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close)
+ GroupMemberInfoView(chatRh, groupInfo, mem, scrollToItemId, stats, code, chatModel, close, close)
}
}
}
@@ -379,6 +436,7 @@ fun ChatView(
chatRh,
type = chatInfo.chatType,
id = chatInfo.apiId,
+ scope = chatInfo.groupChatScope(),
itemIds = listOf(itemId),
mode = mode
)
@@ -397,14 +455,13 @@ fun ChatView(
if (deletedItem.isActiveReport) {
chatModel.chatsContext.decreaseGroupReportsCounter(chatRh, chatInfo.id)
}
+ chatModel.chatsContext.updateChatInfo(chatRh, deleted.deletedChatItem.chatInfo)
}
withContext(Dispatchers.Main) {
- if (deletedChatItem.isReport) {
- if (toChatItem != null) {
- chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem)
- } else {
- chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, deletedChatItem)
- }
+ if (toChatItem != null) {
+ chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem)
+ } else {
+ chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, deletedChatItem)
}
}
}
@@ -512,6 +569,7 @@ fun ChatView(
rh = chatRh,
type = cInfo.chatType,
id = cInfo.apiId,
+ scope = cInfo.groupChatScope(),
itemId = cItem.id,
add = add,
reaction = reaction
@@ -521,16 +579,14 @@ fun ChatView(
chatModel.chatsContext.updateChatItem(cInfo, updatedCI)
}
withContext(Dispatchers.Main) {
- if (cItem.isReport) {
- chatModel.secondaryChatsContext.value?.updateChatItem(cInfo, updatedCI)
- }
+ chatModel.secondaryChatsContext.value?.updateChatItem(cInfo, updatedCI)
}
}
}
},
showItemDetails = { cInfo, cItem ->
suspend fun loadChatItemInfo(): ChatItemInfo? = coroutineScope {
- val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id)
+ val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cInfo.groupChatScope(), cItem.id)
if (ciInfo != null) {
if (chatInfo is ChatInfo.Group) {
setGroupMembers(chatRh, chatInfo.groupInfo, chatModel)
@@ -579,12 +635,16 @@ fun ChatView(
withContext(Dispatchers.Main) {
chatModel.chatsContext.markChatItemsRead(chatRh, chatInfo.id, itemsIds)
ntfManager.cancelNotificationsForChat(chatInfo.id)
- chatModel.controller.apiChatItemsRead(
+ val updatedChatInfo = chatModel.controller.apiChatItemsRead(
chatRh,
chatInfo.chatType,
chatInfo.apiId,
+ chatInfo.groupChatScope(),
itemsIds
)
+ if (updatedChatInfo != null) {
+ chatModel.chatsContext.updateChatInfo(chatRh, updatedChatInfo)
+ }
}
withContext(Dispatchers.Main) {
chatModel.secondaryChatsContext.value?.markChatItemsRead(chatRh, chatInfo.id, itemsIds)
@@ -599,7 +659,8 @@ fun ChatView(
chatModel.controller.apiChatRead(
chatRh,
chatInfo.chatType,
- chatInfo.apiId
+ chatInfo.apiId,
+ chatInfo.groupChatScope()
)
}
withContext(Dispatchers.Main) {
@@ -682,7 +743,8 @@ fun ChatLayout(
selectedChatItems: MutableState?>,
back: () -> Unit,
info: () -> Unit,
- showGroupReports: () -> Unit,
+ showReports: () -> Unit,
+ showSupportChats: () -> Unit,
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
@@ -745,7 +807,7 @@ fun ChatLayout(
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
) {
val composeViewHeight = remember { mutableStateOf(0.dp) }
- Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer, drawWallpaper = chatsCtx.contentTag == null)) {
+ Box(Modifier.fillMaxSize().chatViewBackgroundModifier(MaterialTheme.colors, MaterialTheme.wallpaper, LocalAppBarHandler.current?.backgroundGraphicsLayerSize, LocalAppBarHandler.current?.backgroundGraphicsLayer, drawWallpaper = chatsCtx.secondaryContextFilter == null)) {
val remoteHostId = remember { remoteHostId }.value
val chatInfo = remember { chatInfo }.value
val oneHandUI = remember { appPrefs.oneHandUI.state }
@@ -773,6 +835,7 @@ fun ChatLayout(
.padding(bottom = composeViewHeight.value)
) {
GroupMentions(
+ chatsCtx = chatsCtx,
rhId = remoteHostId,
composeState = composeState,
composeViewFocusRequester = composeViewFocusRequester,
@@ -832,41 +895,76 @@ fun ChatLayout(
}
}
val reportsCount = reportsCount(chatInfo?.id)
+ val supportUnreadCount = supportUnreadCount(chatInfo?.id)
if (oneHandUI.value && chatBottomBar.value) {
- if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate && chatsCtx.contentTag == null && reportsCount > 0) {
- ReportedCountToolbar(reportsCount, withStatusBar = true, showGroupReports)
+ if (
+ chatInfo is ChatInfo.Group
+ && chatsCtx.secondaryContextFilter == null
+ && (reportsCount > 0 || supportUnreadCount > 0)
+ ) {
+ SupportChatsCountToolbar(chatInfo, reportsCount, supportUnreadCount, withStatusBar = true, showReports, showSupportChats)
} else {
StatusBarBackground()
}
} else {
NavigationBarBackground(true, oneHandUI.value, noAlpha = true)
}
- if (chatsCtx.contentTag == MsgContentTag.Report) {
- if (oneHandUI.value) {
- StatusBarBackground()
- }
- Column(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
- Box {
- if (selectedChatItems.value == null) {
- GroupReportsAppBar(chatsCtx, { ModalManager.end.closeModal() }, onSearchValueChanged)
- } else {
- SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value)
- }
- }
- }
- } else {
- Column(if (oneHandUI.value && chatBottomBar.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
- Box {
- if (selectedChatItems.value == null) {
- if (chatInfo != null) {
- ChatInfoToolbar(chatsCtx, chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
+ when (chatsCtx.secondaryContextFilter) {
+ is SecondaryContextFilter.GroupChatScopeContext -> {
+ when (chatsCtx.secondaryContextFilter.groupScopeInfo) {
+ is GroupChatScopeInfo.MemberSupport -> {
+ if (oneHandUI.value) {
+ StatusBarBackground()
+ }
+ Column(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
+ Box {
+ if (selectedChatItems.value == null) {
+ MemberSupportChatAppBar(chatsCtx, chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_, { ModalManager.end.closeModal() }, onSearchValueChanged)
+ } else {
+ SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value)
+ }
+ }
}
- } else {
- SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value)
}
}
- if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate && chatsCtx.contentTag == null && reportsCount > 0 && (!oneHandUI.value || !chatBottomBar.value)) {
- ReportedCountToolbar(reportsCount, withStatusBar = false, showGroupReports)
+ }
+ is SecondaryContextFilter.MsgContentTagContext -> {
+ when (chatsCtx.secondaryContextFilter.contentTag) {
+ MsgContentTag.Report -> {
+ if (oneHandUI.value) {
+ StatusBarBackground()
+ }
+ Column(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
+ Box {
+ if (selectedChatItems.value == null) {
+ GroupReportsAppBar(chatsCtx, { ModalManager.end.closeModal() }, onSearchValueChanged)
+ } else {
+ SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value)
+ }
+ }
+ }
+ }
+ else -> TODO()
+ }
+ }
+ null -> {
+ Column(if (oneHandUI.value && chatBottomBar.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
+ Box {
+ if (selectedChatItems.value == null) {
+ if (chatInfo != null) {
+ ChatInfoToolbar(chatsCtx, chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch)
+ }
+ } else {
+ SelectedItemsCounterToolbar(selectedChatItems, !oneHandUI.value || !chatBottomBar.value)
+ }
+ }
+ if (
+ chatInfo is ChatInfo.Group
+ && (reportsCount > 0 || supportUnreadCount > 0)
+ && (!oneHandUI.value || !chatBottomBar.value)
+ ) {
+ SupportChatsCountToolbar(chatInfo, reportsCount, supportUnreadCount, withStatusBar = false, showReports, showSupportChats)
+ }
}
}
}
@@ -900,7 +998,7 @@ fun BoxScope.ChatInfoToolbar(
showSearch.value = false
}
}
- if (appPlatform.isAndroid && chatsCtx.contentTag == null) {
+ if (appPlatform.isAndroid && chatsCtx.secondaryContextFilter == null) {
BackHandler(onBack = onBackClicked)
}
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
@@ -1097,33 +1195,79 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
}
@Composable
-private fun ReportedCountToolbar(
+private fun SupportChatsCountToolbar(
+ chatInfo: ChatInfo,
reportsCount: Int,
+ supportUnreadCount: Int,
withStatusBar: Boolean,
- showGroupReports: () -> Unit
+ showReports: () -> Unit,
+ showSupportChats: () -> Unit
) {
Box {
val statusBarPadding = if (withStatusBar) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else 0.dp
Row(
Modifier
- .fillMaxWidth()
- .height(AppBarHeight * fontSizeSqrtMultiplier + statusBarPadding)
- .background(MaterialTheme.colors.background)
- .clickable(onClick = showGroupReports)
- .padding(top = statusBarPadding),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
+ .fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly,
) {
- Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.error)
- Spacer(Modifier.width(4.dp))
- Text(
- if (reportsCount == 1) {
- stringResource(MR.strings.group_reports_active_one)
- } else {
- stringResource(MR.strings.group_reports_active).format(reportsCount)
- },
- style = MaterialTheme.typography.button
- )
+ if (
+ chatInfo is ChatInfo.Group
+ && chatInfo.groupInfo.canModerate
+ && reportsCount > 0
+ ) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .weight(1F)
+ .height(AppBarHeight * fontSizeSqrtMultiplier + statusBarPadding)
+ .background(MaterialTheme.colors.background)
+ .clickable(onClick = showReports)
+ .padding(top = statusBarPadding),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.error)
+ Spacer(Modifier.width(4.dp))
+ Text(
+ if (reportsCount == 1) {
+ stringResource(MR.strings.group_reports_active_one)
+ } else {
+ stringResource(MR.strings.group_reports_active).format(reportsCount)
+ },
+ style = MaterialTheme.typography.button
+ )
+ }
+ }
+
+ if (supportUnreadCount > 0) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .weight(1F)
+ .height(AppBarHeight * fontSizeSqrtMultiplier + statusBarPadding)
+ .background(MaterialTheme.colors.background)
+ .clickable(onClick = showSupportChats)
+ .padding(top = statusBarPadding),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.primary)
+ Spacer(Modifier.width(4.dp))
+ Text(
+ if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate) {
+ if (appPlatform.isAndroid)
+ stringResource(MR.strings.group_new_support_chats_short).format(supportUnreadCount)
+ else if (supportUnreadCount == 1)
+ stringResource(MR.strings.group_new_support_chat_one)
+ else
+ stringResource(MR.strings.group_new_support_chats).format(supportUnreadCount)
+ } else {
+ stringResource(MR.strings.group_new_support_messages).format(supportUnreadCount)
+ },
+ style = MaterialTheme.typography.button
+ )
+ }
+ }
}
Divider(Modifier.align(Alignment.BottomStart))
}
@@ -1198,298 +1342,281 @@ fun BoxScope.ChatItemsList(
val searchValueIsNotBlank = remember { derivedStateOf { searchValue.value.isNotBlank() } }
val revealedItems = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(setOf()) }
// not using reversedChatItems inside to prevent possible derivedState bug in Compose when one derived state access can cause crash asking another derived state
- if (chatsCtx != null) {
- val mergedItems = remember {
- derivedStateOf {
- MergedItems.create(chatsCtx.chatItems.value.asReversed(), unreadCount, revealedItems.value, chatsCtx.chatState)
- }
+ val mergedItems = remember {
+ derivedStateOf {
+ MergedItems.create(chatsCtx.chatItems.value.asReversed(), unreadCount, revealedItems.value, chatsCtx.chatState)
}
- val reversedChatItems = remember { derivedStateOf { chatsCtx.chatItems.value.asReversed() } }
- val reportsCount = reportsCount(chatInfo.id)
- val topPaddingToContent = topPaddingToContent(
- chatView = chatsCtx.contentTag == null,
- additionalTopBar = chatsCtx.contentTag == null && reportsCount > 0
- )
- val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent.roundToPx() })
- val numberOfBottomAppBars = numberOfBottomAppBars()
+ }
+ val reversedChatItems = remember { derivedStateOf { chatsCtx.chatItems.value.asReversed() } }
+ val reportsCount = reportsCount(chatInfo.id)
+ val supportUnreadCount = supportUnreadCount(chatInfo.id)
+ val topPaddingToContent = topPaddingToContent(
+ chatView = chatsCtx.secondaryContextFilter == null,
+ additionalTopBar = chatsCtx.secondaryContextFilter == null && (reportsCount > 0 || supportUnreadCount > 0)
+ )
+ val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent.roundToPx() })
+ val numberOfBottomAppBars = numberOfBottomAppBars()
- /** determines height based on window info and static height of two AppBars. It's needed because in the first graphic frame height of
- * [composeViewHeight] is unknown, but we need to set scroll position for unread messages already so it will be correct before the first frame appears
- * */
- val maxHeightForList = rememberUpdatedState(
- with(LocalDensity.current) { LocalWindowHeight().roundToPx() - topPaddingToContentPx.value - (AppBarHeight * fontSizeSqrtMultiplier * numberOfBottomAppBars).roundToPx() }
- )
- val resetListState = remember { mutableStateOf(false) }
- remember(chatModel.openAroundItemId.value) {
- if (chatModel.openAroundItemId.value != null) {
- closeSearch()
- resetListState.value = !resetListState.value
- }
+ /** determines height based on window info and static height of two AppBars. It's needed because in the first graphic frame height of
+ * [composeViewHeight] is unknown, but we need to set scroll position for unread messages already so it will be correct before the first frame appears
+ * */
+ val maxHeightForList = rememberUpdatedState(
+ with(LocalDensity.current) { LocalWindowHeight().roundToPx() - topPaddingToContentPx.value - (AppBarHeight * fontSizeSqrtMultiplier * numberOfBottomAppBars).roundToPx() }
+ )
+ val resetListState = remember { mutableStateOf(false) }
+ remember(chatModel.openAroundItemId.value) {
+ if (chatModel.openAroundItemId.value != null) {
+ closeSearch()
+ resetListState.value = !resetListState.value
}
- val highlightedItems = remember { mutableStateOf(setOf()) }
- val hoveredItemId = remember { mutableStateOf(null as Long?) }
- val listState = rememberUpdatedState(rememberSaveable(chatInfo.id, searchValueIsEmpty.value, resetListState.value, saver = LazyListState.Saver) {
- val openAroundItemId = chatModel.openAroundItemId.value
- val index = mergedItems.value.indexInParentItems[openAroundItemId] ?: mergedItems.value.items.indexOfLast { it.hasUnread() }
- val reportsState = reportsListState
- if (openAroundItemId != null) {
- highlightedItems.value += openAroundItemId
- chatModel.openAroundItemId.value = null
- }
- hoveredItemId.value = null
- if (reportsState != null) {
- reportsListState = null
- reportsState
- } else if (index <= 0 || !searchValueIsEmpty.value) {
- LazyListState(0, 0)
- } else {
- LazyListState(index + 1, -maxHeightForList.value)
- }
- })
- SaveReportsStateOnDispose(chatsCtx, listState)
- val maxHeight = remember { derivedStateOf { listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } }
- val loadingMoreItems = remember { mutableStateOf(false) }
- val animatedScrollingInProgress = remember { mutableStateOf(false) }
- val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf() }
- LaunchedEffect(chatInfo.id, searchValueIsEmpty.value) {
- if (searchValueIsEmpty.value && reversedChatItems.value.size < ChatPagination.INITIAL_COUNT)
- ignoreLoadingRequests.add(reversedChatItems.value.lastOrNull()?.id ?: return@LaunchedEffect)
+ }
+ val highlightedItems = remember { mutableStateOf(setOf()) }
+ val hoveredItemId = remember { mutableStateOf(null as Long?) }
+ val listState = rememberUpdatedState(rememberSaveable(chatInfo.id, searchValueIsEmpty.value, resetListState.value, saver = LazyListState.Saver) {
+ val openAroundItemId = chatModel.openAroundItemId.value
+ val index = mergedItems.value.indexInParentItems[openAroundItemId] ?: mergedItems.value.items.indexOfLast { it.hasUnread() }
+ val reportsState = reportsListState
+ if (openAroundItemId != null) {
+ highlightedItems.value += openAroundItemId
+ chatModel.openAroundItemId.value = null
}
- PreloadItems(chatsCtx, chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), loadingMoreItems, resetListState, mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination ->
- if (loadingMoreItems.value || chatId != chatModel.chatId.value) return@PreloadItems false
- loadingMoreItems.value = true
- withContext(NonCancellable) {
- try {
- loadMessages(chatId, pagination) {
- visibleItemIndexesNonReversed(mergedItems, reversedChatItems.value.size, listState.value)
- }
- } finally {
- loadingMoreItems.value = false
+ hoveredItemId.value = null
+ if (reportsState != null) {
+ reportsListState = null
+ reportsState
+ } else if (index <= 0 || !searchValueIsEmpty.value) {
+ LazyListState(0, 0)
+ } else {
+ LazyListState(index + 1, -maxHeightForList.value)
+ }
+ })
+ SaveReportsStateOnDispose(chatsCtx, listState)
+ val maxHeight = remember { derivedStateOf { listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } }
+ val loadingMoreItems = remember { mutableStateOf(false) }
+ val animatedScrollingInProgress = remember { mutableStateOf(false) }
+ val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf() }
+ LaunchedEffect(chatInfo.id, searchValueIsEmpty.value) {
+ if (searchValueIsEmpty.value && reversedChatItems.value.size < ChatPagination.INITIAL_COUNT)
+ ignoreLoadingRequests.add(reversedChatItems.value.lastOrNull()?.id ?: return@LaunchedEffect)
+ }
+ PreloadItems(chatsCtx, chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), loadingMoreItems, resetListState, mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination ->
+ if (loadingMoreItems.value || chatId != chatModel.chatId.value) return@PreloadItems false
+ loadingMoreItems.value = true
+ withContext(NonCancellable) {
+ try {
+ loadMessages(chatId, pagination) {
+ visibleItemIndexesNonReversed(mergedItems, reversedChatItems.value.size, listState.value)
}
+ } finally {
+ loadingMoreItems.value = false
}
- true
}
- val remoteHostIdUpdated = rememberUpdatedState(remoteHostId)
- val chatInfoUpdated = rememberUpdatedState(chatInfo)
- val scope = rememberCoroutineScope()
- val scrollToItem: (Long) -> Unit = remember {
- // In group reports just set the itemId to scroll to so the main ChatView will handle scrolling
- if (chatsCtx.contentTag == MsgContentTag.Report) return@remember { scrollToItemId.value = it }
- scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages)
- }
- val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(chatsCtx, remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem) }
- if (chatsCtx.contentTag == null) {
- LaunchedEffect(Unit) {
- snapshotFlow { scrollToItemId.value }.filterNotNull().collect {
- if (appPlatform.isAndroid) {
- ModalManager.end.closeModals()
- }
- scrollToItem(it)
- scrollToItemId.value = null
+ true
+ }
+ val remoteHostIdUpdated = rememberUpdatedState(remoteHostId)
+ val chatInfoUpdated = rememberUpdatedState(chatInfo)
+ val scope = rememberCoroutineScope()
+ val scrollToItem: (Long) -> Unit = remember {
+ scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages)
+ }
+ val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(chatsCtx, remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem, scrollToItemId) }
+ if (chatsCtx.secondaryContextFilter == null) {
+ LaunchedEffect(Unit) {
+ snapshotFlow { scrollToItemId.value }.filterNotNull().collect {
+ if (appPlatform.isAndroid) {
+ ModalManager.end.closeModals()
}
+ scrollToItem(it)
+ scrollToItemId.value = null
}
}
- SmallScrollOnNewMessage(listState, reversedChatItems)
- val finishedInitialComposition = remember { mutableStateOf(false) }
- NotifyChatListOnFinishingComposition(finishedInitialComposition, chatInfo, revealedItems, listState, onComposed)
+ }
+ SmallScrollOnNewMessage(listState, reversedChatItems)
+ val finishedInitialComposition = remember { mutableStateOf(false) }
+ NotifyChatListOnFinishingComposition(finishedInitialComposition, chatInfo, revealedItems, listState, onComposed)
- DisposableEffectOnGone(
- whenGone = {
- VideoPlayerHolder.releaseAll()
- }
- )
+ DisposableEffectOnGone(
+ whenGone = {
+ VideoPlayerHolder.releaseAll()
+ }
+ )
- @Composable
- fun ChatViewListItem(
- itemAtZeroIndexInWholeList: Boolean,
- range: State,
- showAvatar: Boolean,
- cItem: ChatItem,
- itemSeparation: ItemSeparation,
- previousItemSeparationLargeGap: Boolean,
- revealed: State,
- reveal: (Boolean) -> Unit
+ @Composable
+ fun ChatViewListItem(
+ itemAtZeroIndexInWholeList: Boolean,
+ range: State,
+ showAvatar: Boolean,
+ cItem: ChatItem,
+ itemSeparation: ItemSeparation,
+ previousItemSeparationLargeGap: Boolean,
+ revealed: State,
+ reveal: (Boolean) -> Unit
+ ) {
+ val itemScope = rememberCoroutineScope()
+ CompositionLocalProvider(
+ // Makes horizontal and vertical scrolling to coexist nicely.
+ // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
+ LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop()
) {
- val itemScope = rememberCoroutineScope()
- CompositionLocalProvider(
- // Makes horizontal and vertical scrolling to coexist nicely.
- // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
- LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop()
- ) {
- val provider = {
- providerForGallery(reversedChatItems.value.asReversed(), cItem.id) { indexInReversed ->
+ val provider = {
+ providerForGallery(reversedChatItems.value.asReversed(), cItem.id) { indexInReversed ->
+ itemScope.launch {
+ listState.value.scrollToItem(
+ min(reversedChatItems.value.lastIndex, indexInReversed + 1),
+ -maxHeight.value
+ )
+ }
+ }
+ }
+
+ @Composable
+ fun ChatItemViewShortHand(cItem: ChatItem, itemSeparation: ItemSeparation, range: State, fillMaxWidth: Boolean = true) {
+ tryOrShowError("${cItem.id}ChatItem", error = {
+ CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
+ }) {
+ val highlighted = remember { derivedStateOf { highlightedItems.value.contains(cItem.id) } }
+ LaunchedEffect(Unit) {
+ snapshotFlow { highlighted.value }
+ .distinctUntilChanged()
+ .filter { it }
+ .collect {
+ delay(500)
+ highlightedItems.value = setOf()
+ }
+ }
+ ChatItemView(chatsCtx, remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, hoveredItemId = hoveredItemId, range = range, searchIsNotBlank = searchValueIsNotBlank, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToItemId = scrollToItemId, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
+ }
+ }
+
+ @Composable
+ fun ChatItemView(cItem: ChatItem, range: State, itemSeparation: ItemSeparation, previousItemSeparationLargeGap: Boolean) {
+ val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
+ if (it == DismissValue.DismissedToStart) {
itemScope.launch {
- listState.value.scrollToItem(
- min(reversedChatItems.value.lastIndex, indexInReversed + 1),
- -maxHeight.value
- )
- }
- }
- }
-
- @Composable
- fun ChatItemViewShortHand(cItem: ChatItem, itemSeparation: ItemSeparation, range: State, fillMaxWidth: Boolean = true) {
- tryOrShowError("${cItem.id}ChatItem", error = {
- CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
- }) {
- val highlighted = remember { derivedStateOf { highlightedItems.value.contains(cItem.id) } }
- LaunchedEffect(Unit) {
- snapshotFlow { highlighted.value }
- .distinctUntilChanged()
- .filter { it }
- .collect {
- delay(500)
- highlightedItems.value = setOf()
- }
- }
- ChatItemView(chatsCtx, remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, hoveredItemId = hoveredItemId, range = range, searchIsNotBlank = searchValueIsNotBlank, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, archiveReports = archiveReports, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
- }
- }
-
- @Composable
- fun ChatItemView(cItem: ChatItem, range: State, itemSeparation: ItemSeparation, previousItemSeparationLargeGap: Boolean) {
- val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
- if (it == DismissValue.DismissedToStart) {
- itemScope.launch {
- if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local && !cItem.isReport) {
- if (composeState.value.editing) {
- composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
- } else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
- composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
- }
+ if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local && !cItem.isReport) {
+ if (composeState.value.editing) {
+ composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
+ } else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
+ composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
}
}
}
- false
}
- val swipeableModifier = SwipeToDismissModifier(
- state = dismissState,
- directions = setOf(DismissDirection.EndToStart),
- swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
- )
- val sent = cItem.chatDir.sent
+ false
+ }
+ val swipeableModifier = SwipeToDismissModifier(
+ state = dismissState,
+ directions = setOf(DismissDirection.EndToStart),
+ swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
+ )
+ val sent = cItem.chatDir.sent
- @Composable
- fun ChatItemBox(modifier: Modifier = Modifier, content: @Composable () -> Unit = { }) {
- Box(
- modifier = modifier.padding(
- bottom = if (itemSeparation.largeGap) {
- if (itemAtZeroIndexInWholeList) {
- 8.dp
- } else {
- 4.dp
- }
- } else 1.dp, top = if (previousItemSeparationLargeGap) 4.dp else 1.dp
- ),
- contentAlignment = Alignment.CenterStart
- ) {
- content()
- }
+ @Composable
+ fun ChatItemBox(modifier: Modifier = Modifier, content: @Composable () -> Unit = { }) {
+ Box(
+ modifier = modifier.padding(
+ bottom = if (itemSeparation.largeGap) {
+ if (itemAtZeroIndexInWholeList) {
+ 8.dp
+ } else {
+ 4.dp
+ }
+ } else 1.dp, top = if (previousItemSeparationLargeGap) 4.dp else 1.dp
+ ),
+ contentAlignment = Alignment.CenterStart
+ ) {
+ content()
}
+ }
- @Composable
- fun adjustTailPaddingOffset(originalPadding: Dp, start: Boolean): Dp {
- val chatItemTail = remember { appPreferences.chatItemTail.state }
- val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true)
- val tailRendered = style is ShapeStyle.Bubble && style.tailVisible
+ @Composable
+ fun adjustTailPaddingOffset(originalPadding: Dp, start: Boolean): Dp {
+ val chatItemTail = remember { appPreferences.chatItemTail.state }
+ val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true)
+ val tailRendered = style is ShapeStyle.Bubble && style.tailVisible
- return originalPadding + (if (tailRendered) 0.dp else if (start) msgTailWidthDp * 2 else msgTailWidthDp)
- }
+ return originalPadding + (if (tailRendered) 0.dp else if (start) msgTailWidthDp * 2 else msgTailWidthDp)
+ }
+
+ Box {
+ val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null
+ val selectionVisible = selectedChatItems.value != null && cItem.canBeDeletedForSelf
+ val selectionOffset by animateDpAsState(if (selectionVisible && !sent) 4.dp + 22.dp * fontSizeMultiplier else 0.dp)
+ val swipeableOrSelectionModifier = (if (selectionVisible) Modifier else swipeableModifier).graphicsLayer { translationX = selectionOffset.toPx() }
+ if (chatInfo is ChatInfo.Group) {
+ if (cItem.chatDir is CIDirection.GroupRcv) {
+ if (showAvatar) {
+ Column(
+ Modifier
+ .padding(top = 8.dp)
+ .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
+ .fillMaxWidth()
+ .then(swipeableModifier),
+ verticalArrangement = Arrangement.spacedBy(4.dp),
+ horizontalAlignment = Alignment.Start
+ ) {
+ @Composable
+ fun MemberNameAndRole(range: State) {
+ Row(Modifier.padding(bottom = 2.dp).graphicsLayer { translationX = selectionOffset.toPx() }, horizontalArrangement = Arrangement.SpaceBetween) {
+ val member = cItem.chatDir.groupMember
+ val rangeValue = range.value
+ val (prevMember, memCount) =
+ if (rangeValue != null) {
+ chatModel.getPrevHiddenMember(member, rangeValue, reversedChatItems.value)
+ } else {
+ null to 1
+ }
+ Text(
+ memberNames(member, prevMember, memCount),
+ Modifier
+ .padding(start = (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + DEFAULT_PADDING_HALF)
+ .weight(1f, false),
+ fontSize = 13.5.sp,
+ color = MaterialTheme.colors.secondary,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1
+ )
+ if (memCount == 1 && member.memberRole > GroupMemberRole.Member) {
+ val chatItemTail = remember { appPreferences.chatItemTail.state }
+ val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true)
+ val tailRendered = style is ShapeStyle.Bubble && style.tailVisible
- Box {
- val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null
- val selectionVisible = selectedChatItems.value != null && cItem.canBeDeletedForSelf
- val selectionOffset by animateDpAsState(if (selectionVisible && !sent) 4.dp + 22.dp * fontSizeMultiplier else 0.dp)
- val swipeableOrSelectionModifier = (if (selectionVisible) Modifier else swipeableModifier).graphicsLayer { translationX = selectionOffset.toPx() }
- if (chatInfo is ChatInfo.Group) {
- if (cItem.chatDir is CIDirection.GroupRcv) {
- if (showAvatar) {
- Column(
- Modifier
- .padding(top = 8.dp)
- .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
- .fillMaxWidth()
- .then(swipeableModifier),
- verticalArrangement = Arrangement.spacedBy(4.dp),
- horizontalAlignment = Alignment.Start
- ) {
- @Composable
- fun MemberNameAndRole(range: State) {
- Row(Modifier.padding(bottom = 2.dp).graphicsLayer { translationX = selectionOffset.toPx() }, horizontalArrangement = Arrangement.SpaceBetween) {
- val member = cItem.chatDir.groupMember
- val rangeValue = range.value
- val (prevMember, memCount) =
- if (rangeValue != null) {
- chatModel.getPrevHiddenMember(member, rangeValue, reversedChatItems.value)
- } else {
- null to 1
- }
Text(
- memberNames(member, prevMember, memCount),
- Modifier
- .padding(start = (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + DEFAULT_PADDING_HALF)
- .weight(1f, false),
+ member.memberRole.text,
+ Modifier.padding(start = DEFAULT_PADDING_HALF * 1.5f, end = DEFAULT_PADDING_HALF + if (tailRendered) msgTailWidthDp else 0.dp),
fontSize = 13.5.sp,
+ fontWeight = FontWeight.Medium,
color = MaterialTheme.colors.secondary,
- overflow = TextOverflow.Ellipsis,
maxLines = 1
)
- if (memCount == 1 && member.memberRole > GroupMemberRole.Member) {
- val chatItemTail = remember { appPreferences.chatItemTail.state }
- val style = shapeStyle(cItem, chatItemTail.value, itemSeparation.largeGap, true)
- val tailRendered = style is ShapeStyle.Bubble && style.tailVisible
-
- Text(
- member.memberRole.text,
- Modifier.padding(start = DEFAULT_PADDING_HALF * 1.5f, end = DEFAULT_PADDING_HALF + if (tailRendered) msgTailWidthDp else 0.dp),
- fontSize = 13.5.sp,
- fontWeight = FontWeight.Medium,
- color = MaterialTheme.colors.secondary,
- maxLines = 1
- )
- }
}
}
-
- @Composable
- fun Item() {
- ChatItemBox(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID)) {
- androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
- SelectedListItem(Modifier, cItem.id, selectedChatItems)
- }
- Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }) {
- val member = cItem.chatDir.groupMember
- Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
- MemberImage(member)
- }
- Box(modifier = Modifier.padding(top = 2.dp, start = 4.dp).chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)) {
- ChatItemViewShortHand(cItem, itemSeparation, range, false)
- }
- }
- }
- }
- if (cItem.content.showMemberName) {
- DependentLayout(Modifier, CHAT_BUBBLE_LAYOUT_ID) {
- MemberNameAndRole(range)
- Item()
- }
- } else {
- Item()
- }
}
- } else {
- ChatItemBox {
- AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
- SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
+
+ @Composable
+ fun Item() {
+ ChatItemBox(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID)) {
+ androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
+ SelectedListItem(Modifier, cItem.id, selectedChatItems)
+ }
+ Row(Modifier.graphicsLayer { translationX = selectionOffset.toPx() }) {
+ val member = cItem.chatDir.groupMember
+ Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
+ MemberImage(member)
+ }
+ Box(modifier = Modifier.padding(top = 2.dp, start = 4.dp).chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)) {
+ ChatItemViewShortHand(cItem, itemSeparation, range, false)
+ }
+ }
}
- Row(
- Modifier
- .padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
- .chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
- .then(swipeableOrSelectionModifier)
- ) {
- ChatItemViewShortHand(cItem, itemSeparation, range)
+ }
+ if (cItem.content.showMemberName) {
+ DependentLayout(Modifier, CHAT_BUBBLE_LAYOUT_ID) {
+ MemberNameAndRole(range)
+ Item()
}
+ } else {
+ Item()
}
}
} else {
@@ -1497,138 +1624,152 @@ fun BoxScope.ChatItemsList(
AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
}
- Box(
+ Row(
Modifier
- .padding(start = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(104.dp, start = true), end = 12.dp)
+ .padding(start = 8.dp + (MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier) + 4.dp, end = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(66.dp, start = false))
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
- .then(if (selectionVisible) Modifier else swipeableModifier)
+ .then(swipeableOrSelectionModifier)
) {
ChatItemViewShortHand(cItem, itemSeparation, range)
}
}
}
- } else { // direct message
+ } else {
ChatItemBox {
AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
}
-
Box(
- Modifier.padding(
- start = if (sent && !voiceWithTransparentBack) adjustTailPaddingOffset(76.dp, start = true) else 12.dp,
- end = if (sent || voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(76.dp, start = false),
- )
+ Modifier
+ .padding(start = if (voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(104.dp, start = true), end = 12.dp)
.chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
- .then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier)
+ .then(if (selectionVisible) Modifier else swipeableModifier)
) {
ChatItemViewShortHand(cItem, itemSeparation, range)
}
}
}
- if (selectionVisible) {
- Box(Modifier.matchParentSize().clickable {
- val checked = selectedChatItems.value?.contains(cItem.id) == true
- selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems, reversedChatItems)
- })
+ } else { // direct message
+ ChatItemBox {
+ AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
+ SelectedListItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
+ }
+
+ Box(
+ Modifier.padding(
+ start = if (sent && !voiceWithTransparentBack) adjustTailPaddingOffset(76.dp, start = true) else 12.dp,
+ end = if (sent || voiceWithTransparentBack) 12.dp else adjustTailPaddingOffset(76.dp, start = false),
+ )
+ .chatItemOffset(cItem, itemSeparation.largeGap, revealed = revealed.value)
+ .then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier)
+ ) {
+ ChatItemViewShortHand(cItem, itemSeparation, range)
+ }
}
}
- }
- if (itemSeparation.date != null) {
- DateSeparator(itemSeparation.date)
- }
- ChatItemView(cItem, range, itemSeparation, previousItemSeparationLargeGap)
- }
- }
- LazyColumnWithScrollBar(
- Modifier.align(Alignment.BottomCenter),
- state = listState.value,
- contentPadding = PaddingValues(
- top = topPaddingToContent,
- bottom = composeViewHeight.value
- ),
- reverseLayout = true,
- additionalBarOffset = composeViewHeight,
- additionalTopBar = rememberUpdatedState(chatsCtx.contentTag == null && reportsCount > 0),
- chatBottomBar = remember { appPrefs.chatBottomBar.state }
- ) {
- val mergedItemsValue = mergedItems.value
- itemsIndexed(mergedItemsValue.items, key = { _, merged -> keyForItem(merged.newest().item) }) { index, merged ->
- val isLastItem = index == mergedItemsValue.items.lastIndex
- val last = if (isLastItem) reversedChatItems.value.lastOrNull() else null
- val listItem = merged.newest()
- val item = listItem.item
- val range = if (merged is MergedItem.Grouped) {
- merged.rangeInReversed.value
- } else {
- null
- }
- val showAvatar = shouldShowAvatar(item, listItem.nextItem)
- val isRevealed = remember { derivedStateOf { revealedItems.value.contains(item.id) } }
- val itemSeparation: ItemSeparation
- val prevItemSeparationLargeGap: Boolean
- if (merged is MergedItem.Single || isRevealed.value) {
- val prev = listItem.prevItem
- itemSeparation = getItemSeparation(item, prev)
- val nextForGap = if ((item.mergeCategory != null && item.mergeCategory == prev?.mergeCategory) || isLastItem) null else listItem.nextItem
- prevItemSeparationLargeGap = if (nextForGap == null) false else getItemSeparationLargeGap(nextForGap, item)
- } else {
- itemSeparation = getItemSeparation(item, null)
- prevItemSeparationLargeGap = false
- }
- ChatViewListItem(index == 0, rememberUpdatedState(range), showAvatar, item, itemSeparation, prevItemSeparationLargeGap, isRevealed) {
- if (merged is MergedItem.Grouped) merged.reveal(it, revealedItems)
- }
-
- if (last != null) {
- // no using separate item(){} block in order to have total number of items in LazyColumn match number of merged items
- DateSeparator(last.meta.itemTs)
- }
- if (item.isRcvNew) {
- val itemIds = when (merged) {
- is MergedItem.Single -> listOf(merged.item.item.id)
- is MergedItem.Grouped -> merged.items.map { it.item.id }
- }
- MarkItemsReadAfterDelay(keyForItem(item), itemIds, finishedInitialComposition, chatInfo.id, listState, markItemsRead)
- }
- }
- }
- FloatingButtons(
- chatsCtx,
- reversedChatItems,
- chatInfoUpdated,
- topPaddingToContent,
- topPaddingToContentPx,
- loadingMoreItems,
- loadingTopItems,
- loadingBottomItems,
- animatedScrollingInProgress,
- mergedItems,
- unreadCount,
- maxHeight,
- composeViewHeight,
- searchValue,
- markChatRead,
- listState,
- loadMessages
- )
- FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent).align(Alignment.TopCenter), topPaddingToContentPx, mergedItems, listState)
-
- LaunchedEffect(Unit) {
- snapshotFlow { listState.value.isScrollInProgress }
- .collect {
- chatViewScrollState.value = it
- }
- }
- LaunchedEffect(Unit) {
- snapshotFlow { listState.value.isScrollInProgress }
- .filter { !it }
- .collect {
- if (animatedScrollingInProgress.value) {
- animatedScrollingInProgress.value = false
+ if (selectionVisible) {
+ Box(Modifier.matchParentSize().clickable {
+ val checked = selectedChatItems.value?.contains(cItem.id) == true
+ selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems, reversedChatItems)
+ })
}
}
+ }
+ if (itemSeparation.date != null) {
+ DateSeparator(itemSeparation.date)
+ }
+ ChatItemView(cItem, range, itemSeparation, previousItemSeparationLargeGap)
}
}
+ LazyColumnWithScrollBar(
+ Modifier.align(Alignment.BottomCenter),
+ state = listState.value,
+ contentPadding = PaddingValues(
+ top = topPaddingToContent,
+ bottom = composeViewHeight.value
+ ),
+ reverseLayout = true,
+ additionalBarOffset = composeViewHeight,
+ additionalTopBar = rememberUpdatedState(chatsCtx.secondaryContextFilter == null && (reportsCount > 0 || supportUnreadCount > 0)),
+ chatBottomBar = remember { appPrefs.chatBottomBar.state }
+ ) {
+ val mergedItemsValue = mergedItems.value
+ itemsIndexed(mergedItemsValue.items, key = { _, merged -> keyForItem(merged.newest().item) }) { index, merged ->
+ val isLastItem = index == mergedItemsValue.items.lastIndex
+ val last = if (isLastItem) reversedChatItems.value.lastOrNull() else null
+ val listItem = merged.newest()
+ val item = listItem.item
+ val range = if (merged is MergedItem.Grouped) {
+ merged.rangeInReversed.value
+ } else {
+ null
+ }
+ val showAvatar = shouldShowAvatar(item, listItem.nextItem)
+ val isRevealed = remember { derivedStateOf { revealedItems.value.contains(item.id) } }
+ val itemSeparation: ItemSeparation
+ val prevItemSeparationLargeGap: Boolean
+ if (merged is MergedItem.Single || isRevealed.value) {
+ val prev = listItem.prevItem
+ itemSeparation = getItemSeparation(item, prev)
+ val nextForGap = if ((item.mergeCategory != null && item.mergeCategory == prev?.mergeCategory) || isLastItem) null else listItem.nextItem
+ prevItemSeparationLargeGap = if (nextForGap == null) false else getItemSeparationLargeGap(nextForGap, item)
+ } else {
+ itemSeparation = getItemSeparation(item, null)
+ prevItemSeparationLargeGap = false
+ }
+ ChatViewListItem(index == 0, rememberUpdatedState(range), showAvatar, item, itemSeparation, prevItemSeparationLargeGap, isRevealed) {
+ if (merged is MergedItem.Grouped) merged.reveal(it, revealedItems)
+ }
+
+ if (last != null) {
+ // no using separate item(){} block in order to have total number of items in LazyColumn match number of merged items
+ DateSeparator(last.meta.itemTs)
+ }
+ if (item.isRcvNew) {
+ val itemIds = when (merged) {
+ is MergedItem.Single -> listOf(merged.item.item.id)
+ is MergedItem.Grouped -> merged.items.map { it.item.id }
+ }
+ MarkItemsReadAfterDelay(keyForItem(item), itemIds, finishedInitialComposition, chatInfo.id, listState, markItemsRead)
+ }
+ }
+ }
+ FloatingButtons(
+ chatsCtx,
+ reversedChatItems,
+ chatInfoUpdated,
+ topPaddingToContent,
+ topPaddingToContentPx,
+ loadingMoreItems,
+ loadingTopItems,
+ loadingBottomItems,
+ animatedScrollingInProgress,
+ mergedItems,
+ unreadCount,
+ maxHeight,
+ composeViewHeight,
+ searchValue,
+ markChatRead,
+ listState,
+ loadMessages
+ )
+ FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent).align(Alignment.TopCenter), topPaddingToContentPx, mergedItems, listState)
+
+ LaunchedEffect(Unit) {
+ snapshotFlow { listState.value.isScrollInProgress }
+ .collect {
+ chatViewScrollState.value = it
+ }
+ }
+ LaunchedEffect(Unit) {
+ snapshotFlow { listState.value.isScrollInProgress }
+ .filter { !it }
+ .collect {
+ if (animatedScrollingInProgress.value) {
+ animatedScrollingInProgress.value = false
+ }
+ }
+ }
}
private suspend fun loadLastItems(chatsCtx: ChatModel.ChatsContext, chatId: State, listState: State, loadItems: State Boolean>) {
@@ -2234,6 +2375,15 @@ fun reportsCount(staleChatId: String?): Int {
}
}
+@Composable
+fun supportUnreadCount(staleChatId: String?): Int {
+ return if (staleChatId?.startsWith("#") != true) {
+ 0
+ } else {
+ remember(staleChatId) { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId } } }.value?.supportUnreadCount ?: 0
+ }
+}
+
private fun reversedChatItemsStatic(chatsCtx: ChatModel.ChatsContext): List =
chatsCtx.chatItems.value.asReversed()
@@ -2315,7 +2465,8 @@ private fun findQuotedItemFromItem(
rhId: State,
chatInfo: State,
scope: CoroutineScope,
- scrollToItem: (Long) -> Unit
+ scrollToItem: (Long) -> Unit,
+ scrollToItemId: MutableState
): (Long) -> Unit = { itemId: Long ->
scope.launch(Dispatchers.Default) {
val item = apiLoadSingleMessage(chatsCtx, rhId.value, chatInfo.value.chatType, chatInfo.value.apiId, itemId)
@@ -2327,7 +2478,11 @@ private fun findQuotedItemFromItem(
chatModel.secondaryChatsContext.value?.updateChatItem(chatInfo.value, item)
}
if (item.quotedItem?.itemId != null) {
- scrollToItem(item.quotedItem.itemId)
+ if (item.isReport && chatsCtx.secondaryContextFilter != null) {
+ scrollToItemId.value = item.quotedItem.itemId
+ } else {
+ scrollToItem(item.quotedItem.itemId)
+ }
} else {
showQuotedItemDoesNotExistAlert()
}
@@ -2499,6 +2654,7 @@ private fun deleteMessages(chatRh: Long?, chatInfo: ChatInfo, itemIds: List
+ chatModel.chatsContext.updateChatInfo(chatRh, updatedChatInfo)
+ }
}
withContext(Dispatchers.Main) {
for (di in deleted) {
- if (di.deletedChatItem.chatItem.isReport) {
- val toChatItem = di.toChatItem?.chatItem
- if (toChatItem != null) {
- chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem)
- } else {
- chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
- }
+ val toChatItem = di.toChatItem?.chatItem
+ if (toChatItem != null) {
+ chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem)
+ } else {
+ chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
}
}
}
@@ -2559,16 +2716,17 @@ private fun archiveReports(chatRh: Long?, chatInfo: ChatInfo, itemIds: List
+ chatModel.chatsContext.updateChatInfo(chatRh, updatedChatInfo)
+ }
}
withContext(Dispatchers.Main) {
for (di in deleted) {
- if (di.deletedChatItem.chatItem.isReport) {
- val toChatItem = di.toChatItem?.chatItem
- if (toChatItem != null) {
- chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem)
- } else {
- chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
- }
+ val toChatItem = di.toChatItem?.chatItem
+ if (toChatItem != null) {
+ chatModel.secondaryChatsContext.value?.upsertChatItem(chatRh, chatInfo, toChatItem)
+ } else {
+ chatModel.secondaryChatsContext.value?.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
}
}
}
@@ -2920,7 +3078,7 @@ fun PreviewChatLayout() {
val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) }
val searchValue = remember { mutableStateOf("") }
ChatLayout(
- chatsCtx = ChatModel.ChatsContext(contentTag = null),
+ chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null),
remoteHostId = remember { mutableStateOf(null) },
chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) },
unreadCount = unreadCount,
@@ -2935,7 +3093,8 @@ fun PreviewChatLayout() {
selectedChatItems = remember { mutableStateOf(setOf()) },
back = {},
info = {},
- showGroupReports = {},
+ showReports = {},
+ showSupportChats = {},
showMemberInfo = { _, _ -> },
loadMessages = { _, _, _ -> },
deleteMessage = { _, _ -> },
@@ -2998,7 +3157,7 @@ fun PreviewGroupChatLayout() {
val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) }
val searchValue = remember { mutableStateOf("") }
ChatLayout(
- chatsCtx = ChatModel.ChatsContext(contentTag = null),
+ chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null),
remoteHostId = remember { mutableStateOf(null) },
chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) },
unreadCount = unreadCount,
@@ -3013,7 +3172,8 @@ fun PreviewGroupChatLayout() {
selectedChatItems = remember { mutableStateOf(setOf()) },
back = {},
info = {},
- showGroupReports = {},
+ showReports = {},
+ showSupportChats = {},
showMemberInfo = { _, _ -> },
loadMessages = { _, _, _ -> },
deleteMessage = { _, _ -> },
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextPendingMemberActionsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextPendingMemberActionsView.kt
new file mode 100644
index 0000000000..3c3f99ad94
--- /dev/null
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextPendingMemberActionsView.kt
@@ -0,0 +1,126 @@
+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.removeMember
+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 {
+ rejectMemberDialog(rhId, member, chatModel, close = { ModalManager.end.closeModal() })
+ },
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(stringResource(MR.strings.reject_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 rejectMemberDialog(rhId: Long?, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
+ AlertManager.shared.showAlertDialog(
+ title = generalGetString(MR.strings.reject_pending_member_alert_title),
+ confirmText = generalGetString(MR.strings.reject_pending_member_button),
+ onConfirm = {
+ removeMember(rhId, member, chatModel, close)
+ },
+ destructive = true,
+ )
+}
+
+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, r.first, r.second)
+ chatModel.chatsContext.updateGroup(rhId, r.first)
+ }
+ }
+ close?.invoke()
+ }
+}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt
index 894bcf3b37..ca6279fd88 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt
@@ -333,7 +333,9 @@ suspend fun MutableState.processPickedMedia(uris: List, text:
@Composable
fun ComposeView(
+ rhId: Long?,
chatModel: ChatModel,
+ chatsCtx: ChatModel.ChatsContext,
chat: Chat,
composeState: MutableState,
attachmentOption: MutableState,
@@ -466,6 +468,7 @@ 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))
@@ -473,7 +476,7 @@ fun ComposeView(
if (!chatItems.isNullOrEmpty()) {
chatItems.forEach { aChatItem ->
withContext(Dispatchers.Main) {
- chatModel.chatsContext.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
+ chatsCtx.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
}
}
return chatItems.first().chatItem
@@ -498,15 +501,17 @@ 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 ->
- chatModel.chatsContext.addChatItem(rhId, chat.chatInfo, chatItem)
+ chatsCtx.addChatItem(rhId, chat.chatInfo, chatItem)
}
}
@@ -563,16 +568,21 @@ 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? {
val cItems = chatModel.controller.apiReportMessage(chat.remoteHostId, chat.chatInfo.apiId, chatItemId, reportReason, msgText)
- if (cItems != null) {
- withContext(Dispatchers.Main) {
- cItems.forEach { chatItem ->
- chatModel.chatsContext.addChatItem(chat.remoteHostId, chat.chatInfo, chatItem.chatItem)
- }
- }
- }
-
+ if (chatModel.controller.appPrefs.showReportsInSupportChatAlert.get()) showReportsInSupportChatAlert()
return cItems?.map { it.chatItem }
}
@@ -581,7 +591,7 @@ fun ComposeView(
val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc)
if (contact != null) {
withContext(Dispatchers.Main) {
- chatModel.chatsContext.updateContact(chat.remoteHostId, contact)
+ chatsCtx.updateContact(chat.remoteHostId, contact)
}
}
}
@@ -594,13 +604,14 @@ 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) {
- chatModel.chatsContext.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem)
+ chatsCtx.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem)
}
}
return updatedItem?.chatItem
@@ -891,7 +902,7 @@ fun ComposeView(
fun editPrevMessage() {
if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return
- val lastEditable = chatModel.chatsContext.chatItems.value.findLast { it.meta.editable }
+ val lastEditable = chatsCtx.chatItems.value.findLast { it.meta.editable }
if (lastEditable != null) {
composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews)
}
@@ -1004,6 +1015,21 @@ fun ComposeView(
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()
}
@@ -1011,8 +1037,8 @@ fun ComposeView(
if (ctx is ComposeContextItem.ReportedItem) {
ReportReasonView(ctx.reason)
}
- val simplexLinkProhibited = hasSimplexLink.value && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)
- val fileProhibited = composeState.value.attachmentPreview && !chat.groupFeatureEnabled(GroupFeature.Files)
+ val simplexLinkProhibited = chatsCtx.secondaryContextFilter == null && hasSimplexLink.value && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)
+ val fileProhibited = chatsCtx.secondaryContextFilter == null && composeState.value.attachmentPreview && !chat.groupFeatureEnabled(GroupFeature.Files)
val voiceProhibited = composeState.value.preview is ComposePreview.VoicePreview && !chat.chatInfo.featureEnabled(ChatFeature.Voice)
if (composeState.value.preview !is ComposePreview.VoicePreview || composeState.value.editing) {
if (simplexLinkProhibited) {
@@ -1041,7 +1067,10 @@ fun ComposeView(
Surface(color = MaterialTheme.colors.background, contentColor = MaterialTheme.colors.onBackground) {
Divider()
Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) {
- val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
+ val isGroupAndProhibitedFiles =
+ chatsCtx.secondaryContextFilter == null
+ && chat.chatInfo is ChatInfo.Group
+ && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
val attachmentClicked = if (isGroupAndProhibitedFiles) {
{
AlertManager.shared.showAlertMsg(
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt
index b9538bc691..ac722783a3 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt
@@ -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(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() })
+ ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, 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) {
+ IconButton({ moderateItems() }, Modifier.alpha(if (canModerate.value) 1f else 0f), enabled = moderateEnabled.value && !deleteCountProhibited.value && chatsCtx.secondaryContextFilter == null) {
Icon(
painterResource(MR.images.ic_flag),
null,
Modifier.size(22.dp),
- tint = if (!moderateEnabled.value || deleteCountProhibited.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error
+ tint = if (!moderateEnabled.value || deleteCountProhibited.value || chatsCtx.secondaryContextFilter != null) MaterialTheme.colors.secondary else MaterialTheme.colors.error
)
}
- IconButton({ forwardItems() }, enabled = forwardEnabled.value && !forwardCountProhibited.value) {
+ IconButton({ forwardItems() }, enabled = forwardEnabled.value && !forwardCountProhibited.value && chatsCtx.secondaryContextFilter == null) {
Icon(
painterResource(MR.images.ic_forward),
null,
Modifier.size(22.dp),
- tint = if (!forwardEnabled.value || forwardCountProhibited.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
+ tint = if (!forwardEnabled.value || forwardCountProhibited.value || chatsCtx.secondaryContextFilter != null) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
)
}
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt
index 10694d13bf..827af085ea 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt
@@ -55,6 +55,16 @@ 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) {
@@ -93,8 +103,9 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List {
.asSequence()
.map { it.chatInfo }
.filterIsInstance()
+ .filter { it.sendMsgEnabled }
.map { it.contact }
- .filter { c -> c.sendMsgEnabled && !c.nextSendGrpInv && c.contactId !in memberContactIds && c.anyNameContains(s)
+ .filter { c -> !c.nextSendGrpInv && c.contactId !in memberContactIds && c.anyNameContains(s)
}
.sortedBy { it.displayName.lowercase() }
.toList()
@@ -110,6 +121,7 @@ fun AddGroupMembersLayout(
allowModifyMembers: Boolean,
searchText: MutableState,
openPreferences: () -> Unit,
+ openMemberAdmission: () -> Unit,
inviteMembers: () -> Unit,
clearSelection: () -> Unit,
addContact: (Long) -> Unit,
@@ -144,7 +156,7 @@ fun AddGroupMembersLayout(
horizontalArrangement = Arrangement.Center
) {
ChatInfoToolbarTitle(
- ChatInfo.Group(groupInfo),
+ ChatInfo.Group(groupInfo, groupChatScope = null),
imageSize = 60.dp,
iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight
)
@@ -165,6 +177,9 @@ fun AddGroupMembersLayout(
} else {
SectionView {
if (creatingGroup) {
+ SectionItemView(openMemberAdmission) {
+ Text(stringResource(MR.strings.set_member_admission))
+ }
SectionItemView(openPreferences) {
Text(stringResource(MR.strings.set_group_preferences))
}
@@ -376,6 +391,7 @@ fun PreviewAddGroupMembersLayout() {
allowModifyMembers = true,
searchText = remember { mutableStateOf(TextFieldValue("")) },
openPreferences = {},
+ openMemberAdmission = {},
inviteMembers = {},
clearSelection = {},
addContact = {},
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt
index 22956738e7..db6eff562e 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt
@@ -74,6 +74,9 @@ 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,
@@ -95,9 +98,7 @@ fun ModalData.GroupChatInfoView(
setChatTTLAlert(chatsCtx, chat.remoteHostId, chat.chatInfo, chatItemTTL, previousChatTTL, deletingItems)
},
- activeSortedMembers = remember { chatModel.groupMembers }.value
- .filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
- .sortedByDescending { it.memberRole },
+ activeSortedMembers = activeSortedMembers,
developerTools,
onLocalAliasChanged = { setGroupAlias(chat, it, chatModel) },
groupLink,
@@ -126,7 +127,7 @@ fun ModalData.GroupChatInfoView(
}
ModalManager.end.showModalCloseable(true) { closeCurrent ->
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
- GroupMemberInfoView(rhId, groupInfo, mem, stats, code, chatModel, closeCurrent) {
+ GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, closeCurrent) {
closeCurrent()
close()
}
@@ -140,6 +141,17 @@ 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(
@@ -317,6 +329,40 @@ fun AddGroupMembersButton(
)
}
+@Composable
+fun UserSupportChatButton(
+ chat: Chat,
+ groupInfo: GroupInfo,
+ scrollToItemId: MutableState
+) {
+ 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,
@@ -337,6 +383,7 @@ fun ModalData.GroupChatInfoLayout(
showMemberInfo: (GroupMember) -> Unit,
editGroupProfile: () -> Unit,
addOrEditWelcomeMessage: () -> Unit,
+ openMemberSupport: () -> Unit,
openPreferences: () -> Unit,
deleteGroup: () -> Unit,
clearChat: () -> Unit,
@@ -422,6 +469,40 @@ 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)
@@ -431,19 +512,17 @@ fun ModalData.GroupChatInfoLayout(
}
val prefsTitleId = if (groupInfo.businessChat == null) MR.strings.group_preferences else MR.strings.chat_preferences
GroupPreferencesButton(prefsTitleId, openPreferences)
- if (groupInfo.canModerate) {
- GroupReportsButton {
- scope.launch {
- showGroupReportsView(chatModel.chatId, scrollToItemId, chat.chatInfo)
- }
- }
- }
+ }
+ 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 (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 } } }
@@ -453,23 +532,13 @@ 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) {
@@ -481,7 +550,7 @@ fun ModalData.GroupChatInfoLayout(
}
if (activeSortedMembers.size > 8) {
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
- SearchRowView(searchText)
+ MemberListSearchRowView(searchText)
}
}
SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
@@ -527,7 +596,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.memberCurrent) {
+ if (groupInfo.membership.memberCurrentOrPending) {
val titleId = if (groupInfo.businessChat == null) MR.strings.button_leave_group else MR.strings.button_leave_chat
LeaveGroupButton(titleId, leaveGroup)
}
@@ -631,17 +700,14 @@ private fun SelectedItemsCounterToolbarSetter(
}
@Composable
-fun ChatTTLSection(chatItemTTL: State, setChatItemTTL: (ChatItemTTL?) -> Unit, deletingItems: State) {
+fun ChatTTLOption(chatItemTTL: State, setChatItemTTL: (ChatItemTTL?) -> Unit, deletingItems: State) {
Box {
- SectionView {
- TtlOptions(
- chatItemTTL,
- enabled = remember { derivedStateOf { !deletingItems.value } },
- onSelected = setChatItemTTL,
- default = chatModel.chatItemTTL
- )
- SectionTextFooter(stringResource(MR.strings.chat_ttl_options_footer))
- }
+ TtlOptions(
+ chatItemTTL,
+ enabled = remember { derivedStateOf { !deletingItems.value } },
+ onSelected = setChatItemTTL,
+ default = chatModel.chatItemTTL
+ )
if (deletingItems.value) {
Box(Modifier.matchParentSize()) {
ProgressIndicator()
@@ -683,6 +749,23 @@ 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(
@@ -693,12 +776,20 @@ private fun GroupPreferencesButton(titleId: StringResource, onClick: () -> Unit)
}
@Composable
-private fun GroupReportsButton(onClick: () -> Unit) {
- SettingsActionItem(
- painterResource(MR.images.ic_flag),
+private fun GroupReportsButton(chat: Chat, onClick: () -> Unit) {
+ SettingsActionItemWithContent(
+ painterResource(if (chat.chatStats.reportsCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag),
stringResource(MR.strings.group_reports_member_reports),
- click = onClick
- )
+ 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
+ )
+ }
+ }
}
@Composable
@@ -820,7 +911,7 @@ fun MemberRow(member: GroupMember, user: Boolean = false, infoPage: Boolean = tr
}
@Composable
-private fun MemberVerifiedShield() {
+fun MemberVerifiedShield() {
Icon(painterResource(MR.images.ic_verified_user), null, Modifier.padding(end = 3.dp).size(16.dp), tint = MaterialTheme.colors.secondary)
}
@@ -941,7 +1032,7 @@ private fun DeleteGroupButton(titleId: StringResource, onClick: () -> Unit) {
}
@Composable
-private fun SearchRowView(
+fun MemberListSearchRowView(
searchText: MutableState = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
) {
Box(Modifier.width(36.dp), contentAlignment = Alignment.Center) {
@@ -964,16 +1055,18 @@ private fun setGroupAlias(chat: Chat, localAlias: String, chatModel: ChatModel)
fun removeMembers(rhId: Long?, groupInfo: GroupInfo, memberIds: List, onSuccess: () -> Unit = {}) {
withBGApi {
- val updatedMembers = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, memberIds)
- if (updatedMembers != null) {
+ val r = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, memberIds)
+ if (r != null) {
+ val (updatedGroupInfo, updatedMembers) = r
withContext(Dispatchers.Main) {
+ chatModel.chatsContext.updateGroup(rhId, updatedGroupInfo)
updatedMembers.forEach { updatedMember ->
- chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, updatedMember)
+ chatModel.chatsContext.upsertGroupMember(rhId, updatedGroupInfo, updatedMember)
}
}
withContext(Dispatchers.Main) {
updatedMembers.forEach { updatedMember ->
- chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, updatedMember)
+ chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, updatedGroupInfo, updatedMember)
}
}
onSuccess()
@@ -1016,7 +1109,18 @@ fun PreviewGroupChatInfoLayout() {
selectedItems = remember { mutableStateOf(null) },
appBar = remember { mutableStateOf(null) },
scrollToItemId = remember { mutableStateOf(null) },
- addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, onSearchClicked = {}, deletingItems = remember { mutableStateOf(true) }
+ addMembers = {},
+ showMemberInfo = {},
+ editGroupProfile = {},
+ addOrEditWelcomeMessage = {},
+ openMemberSupport = {},
+ openPreferences = {},
+ deleteGroup = {},
+ clearChat = {},
+ leaveGroup = {},
+ manageGroupLink = {},
+ onSearchClicked = {},
+ deletingItems = remember { mutableStateOf(true) }
)
}
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt
index 285c96165c..e56bc36562 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt
@@ -45,6 +45,7 @@ fun GroupMemberInfoView(
rhId: Long?,
groupInfo: GroupInfo,
member: GroupMember,
+ scrollToItemId: MutableState,
connectionStats: ConnectionStats?,
connectionCode: String?,
chatModel: ChatModel,
@@ -79,6 +80,7 @@ fun GroupMemberInfoView(
rhId = rhId,
groupInfo,
member,
+ scrollToItemId,
connStats,
newRole,
developerTools,
@@ -243,32 +245,34 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c
text = generalGetString(messageId),
confirmText = generalGetString(MR.strings.remove_member_confirmation),
onConfirm = {
- withBGApi {
- val removedMembers = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId))
- if (removedMembers != null) {
- withContext(Dispatchers.Main) {
- removedMembers.forEach { removedMember ->
- chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, removedMember)
- }
- }
- withContext(Dispatchers.Main) {
- removedMembers.forEach { removedMember ->
- chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, removedMember)
- }
- }
- }
- close?.invoke()
- }
+ removeMember(rhId, member, chatModel, close)
},
destructive = true,
)
}
+fun removeMember(rhId: Long?, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
+ withBGApi {
+ val r = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId))
+ if (r != null) {
+ val (updatedGroupInfo, removedMembers) = r
+ withContext(Dispatchers.Main) {
+ chatModel.chatsContext.updateGroup(rhId, updatedGroupInfo)
+ removedMembers.forEach { removedMember ->
+ chatModel.chatsContext.upsertGroupMember(rhId, updatedGroupInfo, removedMember)
+ }
+ }
+ }
+ close?.invoke()
+ }
+}
+
@Composable
fun GroupMemberInfoLayout(
rhId: Long?,
groupInfo: GroupInfo,
member: GroupMember,
+ scrollToItemId: MutableState,
connStats: MutableState,
newRole: MutableState,
developerTools: Boolean,
@@ -299,6 +303,29 @@ 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)
@@ -413,6 +440,12 @@ 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)
}
@@ -878,6 +911,7 @@ 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,
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt
index 2c4d4b16a8..62f1a4337c 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMembersToolbar.kt
@@ -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(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() })
+ ComposeView(rhId = null, chatModel = chatModel, chatModel.chatsContext, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}, remember { FocusRequester() })
}
Row(
Modifier
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt
index 91f4f5173c..aa737a02d3 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMentions.kt
@@ -35,6 +35,7 @@ private val MAX_PICKER_HEIGHT = (PICKER_ROW_SIZE * 4) + (MEMBER_ROW_AVATAR_SIZE
@Composable
fun GroupMentions(
+ chatsCtx: ChatModel.ChatsContext,
rhId: Long?,
composeState: MutableState,
composeViewFocusRequester: FocusRequester?,
@@ -48,12 +49,31 @@ fun GroupMentions(
val mentionName = remember { mutableStateOf("") }
val mentionRange = remember { mutableStateOf(null) }
val mentionMemberId = remember { mutableStateOf(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 }
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt
index 12c5b65769..b8db5969a1 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt
@@ -17,7 +17,9 @@ 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> = listOf(
@@ -71,6 +73,16 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () ->
preferences = currentPreferences
},
savePrefs = ::savePrefs,
+ openMemberAdmission = {
+ ModalManager.end.showCustomModal { close ->
+ MemberAdmissionView(
+ chatModel,
+ rhId,
+ chatId,
+ close
+ )
+ }
+ }
)
}
}
@@ -83,10 +95,15 @@ 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)))
@@ -156,6 +173,15 @@ 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,
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt
index 1eeeb99c93..2cc2402c0a 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt
@@ -15,7 +15,15 @@ import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.flow.*
@Composable
-private fun GroupReportsView(reportsChatsCtx: ChatModel.ChatsContext, staleChatId: State, scrollToItemId: MutableState) {
+private fun GroupReportsView(
+ reportsChatsCtx: ChatModel.ChatsContext,
+ staleChatId: State,
+ scrollToItemId: MutableState,
+ close: () -> Unit
+) {
+ KeyChangeEffect(chatModel.chatId.value) {
+ close()
+ }
ChatView(reportsChatsCtx, staleChatId, scrollToItemId, onComposed = {})
}
@@ -53,7 +61,7 @@ fun GroupReportsAppBar(
}
@Composable
-private fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) {
+fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) {
LaunchedEffect(Unit) {
snapshotFlow { chatModel.chatId.value }
.distinctUntilChanged()
@@ -69,13 +77,13 @@ private fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) {
}
suspend fun showGroupReportsView(staleChatId: State, scrollToItemId: MutableState, chatInfo: ChatInfo) {
- val reportsChatsCtx = ChatModel.ChatsContext(contentTag = MsgContentTag.Report)
+ val reportsChatsCtx = ChatModel.ChatsContext(secondaryContextFilter = SecondaryContextFilter.MsgContentTagContext(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)
+ GroupReportsView(reportsChatsCtx, staleChatId, scrollToItemId, close)
} else {
LaunchedEffect(Unit) {
close()
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt
new file mode 100644
index 0000000000..48171bfeb7
--- /dev/null
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberAdmission.kt
@@ -0,0 +1,152 @@
+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> = 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,
+ 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,
+ )
+}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt
new file mode 100644
index 0000000000..99e2e3198e
--- /dev/null
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt
@@ -0,0 +1,145 @@
+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,
+ scrollToItemId: MutableState
+) {
+ 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, scrollToItemId: MutableState, 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()
+ }
+ }
+ }
+ }
+}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt
new file mode 100644
index 0000000000..298a545c8c
--- /dev/null
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt
@@ -0,0 +1,302 @@
+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.*
+
+@Composable
+fun ModalData.MemberSupportView(
+ rhId: Long?,
+ chat: Chat,
+ groupInfo: GroupInfo,
+ scrollToItemId: MutableState,
+ 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
+) {
+ 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 { 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) {
+ 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
+ })
+ } else {
+ ItemAction(stringResource(MR.strings.delete_member_support_chat_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
+ deleteMemberSupportChatDialog(rhId, groupInfo, member)
+ showMenu.value = false
+ })
+ }
+ }
+}
+
+fun deleteMemberSupportChatDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember) {
+ AlertManager.shared.showAlertDialog(
+ title = generalGetString(MR.strings.delete_member_support_chat_alert_title),
+ confirmText = generalGetString(MR.strings.delete_member_support_chat_button),
+ onConfirm = {
+ deleteMemberSupportChat(rhId, groupInfo, member)
+ },
+ destructive = true,
+ )
+}
+
+private fun deleteMemberSupportChat(rhId: Long?, groupInfo: GroupInfo, member: GroupMember) {
+ withBGApi {
+ val r = chatModel.controller.apiDeleteMemberSupportChat(rhId, groupInfo.groupId, member.groupMemberId)
+ if (r != null) {
+ withContext(Dispatchers.Main) {
+ chatModel.chatsContext.upsertGroupMember(rhId, r.first, r.second)
+ chatModel.chatsContext.updateGroup(rhId, r.first)
+ }
+ }
+ }
+}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt
index 2e789df7bc..6e938aa5c4 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt
@@ -86,6 +86,7 @@ fun ChatItemView(
joinGroup: (Long, () -> Unit) -> Unit,
acceptCall: (Contact) -> Unit,
scrollToItem: (Long) -> Unit,
+ scrollToItemId: MutableState,
scrollToQuotedItemFromItem: (Long) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
openDirectChat: (Long) -> Unit,
@@ -271,6 +272,7 @@ fun ChatItemView(
}
}
+ // improvement could be to track "forwarded from" scope and open it
@Composable
fun GoToItemButton(alignStart: Boolean, parentActivated: State) {
val chatTypeApiIdMsgId = cItem.meta.itemForwarded?.chatTypeApiIdMsgId
@@ -324,7 +326,7 @@ fun ChatItemView(
) {
@Composable
fun framedItemView() {
- FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem, scrollToQuotedItemFromItem)
+ FramedItemView(chatsCtx, cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, tailVisible = itemSeparation.largeGap, receiveFile, onLinkLongClick, scrollToItem, scrollToItemId, scrollToQuotedItemFromItem)
}
fun deleteMessageQuestionText(): String {
@@ -635,6 +637,15 @@ 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)
@@ -711,12 +722,16 @@ fun ChatItemView(
is CIContent.RcvGroupEventContent -> {
when (c.rcvGroupEvent) {
is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat)
+ is RcvGroupEvent.NewMemberPendingReview -> PendingReviewEventItemView()
else -> EventItemView()
}
MsgContentItemDropdownMenu()
}
is CIContent.SndGroupEventContent -> {
- EventItemView()
+ when (c.sndGroupEvent) {
+ is SndGroupEvent.UserPendingReview -> PendingReviewEventItemView()
+ else -> EventItemView()
+ }
MsgContentItemDropdownMenu()
}
is CIContent.RcvConnEventContent -> {
@@ -1422,7 +1437,7 @@ fun PreviewChatItemView(
chatItem: ChatItem = ChatItem.getSampleData(1, CIDirection.DirectSnd(), Clock.System.now(), "hello")
) {
ChatItemView(
- chatsCtx = ChatModel.ChatsContext(contentTag = null),
+ chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null),
rhId = null,
ChatInfo.Direct.sampleData,
chatItem,
@@ -1444,6 +1459,7 @@ fun PreviewChatItemView(
joinGroup = { _, _ -> },
acceptCall = { _ -> },
scrollToItem = {},
+ scrollToItemId = remember { mutableStateOf(null) },
scrollToQuotedItemFromItem = {},
acceptFeature = { _, _, _ -> },
openDirectChat = { _ -> },
@@ -1472,7 +1488,7 @@ fun PreviewChatItemView(
fun PreviewChatItemViewDeletedContent() {
SimpleXTheme {
ChatItemView(
- chatsCtx = ChatModel.ChatsContext(contentTag = null),
+ chatsCtx = ChatModel.ChatsContext(secondaryContextFilter = null),
rhId = null,
ChatInfo.Direct.sampleData,
ChatItem.getDeletedContentSampleData(),
@@ -1494,6 +1510,7 @@ fun PreviewChatItemViewDeletedContent() {
joinGroup = { _, _ -> },
acceptCall = { _ -> },
scrollToItem = {},
+ scrollToItemId = remember { mutableStateOf(null) },
scrollToQuotedItemFromItem = {},
acceptFeature = { _, _, _ -> },
openDirectChat = { _ -> },
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt
index fd8a32af64..b2beba29e4 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt
@@ -27,6 +27,7 @@ import kotlin.math.ceil
@Composable
fun FramedItemView(
+ chatsCtx: ChatModel.ChatsContext,
chatInfo: ChatInfo,
ci: ChatItem,
uriHandler: UriHandler? = null,
@@ -39,6 +40,7 @@ fun FramedItemView(
receiveFile: (Long) -> Unit,
onLinkLongClick: (link: String) -> Unit = {},
scrollToItem: (Long) -> Unit = {},
+ scrollToItemId: MutableState,
scrollToQuotedItemFromItem: (Long) -> Unit = {},
) {
val sent = ci.chatDir.sent
@@ -253,7 +255,11 @@ fun FramedItemView(
onLongClick = { showMenu.value = true },
onClick = {
if (ci.quotedItem.itemId != null) {
- scrollToItem(ci.quotedItem.itemId)
+ if (ci.isReport && chatsCtx.secondaryContextFilter != null) {
+ scrollToItemId.value = ci.quotedItem.itemId
+ } else {
+ scrollToItem(ci.quotedItem.itemId)
+ }
} else {
scrollToQuotedItemFromItem(ci.id)
}
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt
index 958b794bd7..52b4059eef 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt
@@ -236,7 +236,6 @@ 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()
@@ -305,7 +304,7 @@ fun GroupMenuItems(
}
}
GroupMemberStatus.MemAccepted -> {
- if (groupInfo.membership.memberCurrent) {
+ if (groupInfo.membership.memberCurrentOrPending) {
LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu)
}
if (groupInfo.canDelete) {
@@ -327,7 +326,7 @@ fun GroupMenuItems(
}
}
ClearChatAction(chat, showMenu)
- if (groupInfo.membership.memberCurrent) {
+ if (groupInfo.membership.memberCurrentOrPending) {
LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu)
}
if (groupInfo.canDelete) {
@@ -615,7 +614,8 @@ fun markChatRead(c: Chat) {
chatModel.controller.apiChatRead(
chat.remoteHostId,
chat.chatInfo.chatType,
- chat.chatInfo.apiId
+ chat.chatInfo.apiId,
+ chat.chatInfo.groupChatScope()
)
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.updateChatTagReadNoContentTag(chat, wasUnread)
+ chatModel.chatsContext.updateChatTagReadInPrimaryContext(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))
+ ChatInfo.Group(groupInfo.copy(chatSettings = chatSettings), groupChatScope = null)
}
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.updateChatTagReadNoContentTag(updatedChat, wasUnread)
+ chatModel.chatsContext.updateChatTagReadInPrimaryContext(updatedChat, wasUnread)
}
}
val current = currentState?.value
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt
index 93d512507a..65a9d175dd 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt
@@ -241,6 +241,8 @@ 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 -> {}
@@ -363,7 +365,11 @@ fun ChatPreviewView(
if (progressByTimeout) {
progressView()
} else if (chat.chatStats.reportsCount > 0) {
- GroupReportsIcon()
+ 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)
} else {
IncognitoIcon(chat.chatInfo.incognito)
}
@@ -465,17 +471,10 @@ fun ChatPreviewView(
)
}
} else {
- 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())
+ UnreadBadge(
+ text = if (n > 0) unreadCountStr(n) else "",
+ backgroundColor = if (disabled || showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant,
+ yOffset = 3.dp
)
}
}
@@ -548,11 +547,11 @@ fun IncognitoIcon(incognito: Boolean) {
}
@Composable
-fun GroupReportsIcon() {
+fun FlagIcon(color: Color) {
Icon(
painterResource(MR.images.ic_flag),
contentDescription = null,
- tint = MaterialTheme.colors.error,
+ tint = color,
modifier = Modifier
.size(21.sp.toDp())
.offset(x = 2.sp.toDp())
@@ -567,6 +566,26 @@ 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)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt
index 185ec3925f..13351a2111 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt
@@ -634,7 +634,7 @@ fun HostDisconnectButton(onClick: (() -> Unit)?) {
}
@Composable
-fun BoxScope.unreadBadge(unreadCount: Int, userMuted: Boolean, hasPadding: Boolean) {
+fun BoxScope.userUnreadBadge(unreadCount: Int, userMuted: Boolean, hasPadding: Boolean) {
Text(
if (unreadCount > 0) unreadCountStr(unreadCount) else "",
color = Color.White,
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt
index 3d913cf957..2db40f770d 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt
@@ -45,7 +45,6 @@ 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)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt
index 7ea656e1e4..5bd45ccaab 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt
@@ -21,7 +21,6 @@ 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.*
@@ -128,9 +127,7 @@ fun SettingsLayout(
SectionDividerSpaced()
SectionView(stringResource(MR.strings.settings_section_title_support)) {
- if (!BuildConfigCommon.ANDROID_BUNDLE) {
- ContributeItem(uriHandler)
- }
+ ContributeItem(uriHandler)
RateAppItem(uriHandler)
StarOnGithubItem(uriHandler)
}
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
index 6726009a5f..b5bf2efaff 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
@@ -158,6 +158,8 @@
Error loading details
Error adding member(s)
Error joining group
+ Error accepting member
+ Error deleting chat with member
Cannot receive file
Sender cancelled file transfer.
Unknown servers!
@@ -464,6 +466,10 @@
1 report
%d reports
Member reports
+ %d messages
+ %d chats with members
+ 1 chat with a member
+ %d chat(s)
Share message…
@@ -486,7 +492,6 @@
Decoding error
The image cannot be decoded. Please, try a different image or contact developers.
The video cannot be decoded. Please, try a different video or contact developers.
- you are observer
Files and media prohibited!
Only group owners can enable files and media.
Send direct message to connect
@@ -505,6 +510,8 @@
Report violation: only group moderators will see it.
Report content: only group moderators will see it.
Report other: only group moderators will see it.
+ Report sent to moderators
+ You can view your reports in Chat with admins.
You can\'t send messages!
contact not ready
@@ -518,6 +525,9 @@
removed from group
you left
can\'t send messages
+ you are observer
+ reviewed by admins
+ member has old version
Image
@@ -1063,6 +1073,7 @@
Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.
Edit image
Delete image
+ Save admission settings?
Save preferences?
Save and notify contact
Save and notify contacts
@@ -1582,6 +1593,8 @@
invited %1$s
connected
+ accepted %1$s
+ accepted you
left
changed role of %s to %s
blocked %s
@@ -1593,6 +1606,7 @@
updated group profile
invited via your group link
connected directly
+ New member wants to join the group.
you changed role of %s to %s
you changed role for yourself to %s
you blocked %s
@@ -1600,6 +1614,8 @@
you removed %1$s
you left
group profile updated
+ you accepted this member
+ Please wait for group moderators to review your request to join the group.
%s connected
%s and %s connected
@@ -1659,6 +1675,8 @@
invited
pending approval
pending
+ pending review
+ review
connecting (introduced)
connecting (introduction invitation)
connecting (accepted)
@@ -1729,6 +1747,7 @@
Receipts are disabled
This group has over %1$d members, delivery receipts are not sent.
Invite
+ Chat with admins
FOR CONSOLE
@@ -1765,7 +1784,7 @@
Remove member?
Remove members?
Remove member
-
+ Chat with member
Send direct message
Member will be removed from group - this cannot be undone!
Members will be removed from group - this cannot be undone!
@@ -2042,6 +2061,7 @@
Contact preferences
Group preferences
Set group preferences
+ Set member admission
Your preferences
Disappearing messages
Direct messages
@@ -2159,6 +2179,29 @@
owners
Enabled for
+
+ Member admission
+ Review members
+ Review members before admitting ("knocking").
+ off
+ all
+
+
+ Chats with members
+ No chats with members
+ Delete chat
+ Delete chat with member?
+
+
+ Chat with admins
+ Reject
+ Reject member?
+ Accept
+ Accept member
+ Member will join the group, accept member?
+ Accept as member
+ Accept as observer
+
What\'s new
New in %s
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml
index 6646720c5c..53f31ecba9 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml
@@ -1014,7 +1014,7 @@
Moderieren
Diese Nachricht wird für alle Mitglieder als moderiert gekennzeichnet.
Sie sind Beobachter
- Sie können keine Nachrichten versenden!
+ Sie sind Beobachter
Beobachter
Anfängliche Rolle
Nachricht des Mitglieds löschen\?
@@ -1465,7 +1465,7 @@
Profil erstellen
%s und %s
Ihrer Gruppe beitreten?
- %1$s.]]>
+ %1$s.]]>
Das ist Ihr eigener Einmal-Link!
%d Nachrichten als gelöscht markiert
Gruppe besteht bereits!
@@ -2462,4 +2462,52 @@
Aus
TCP-Port 443 nur für voreingestellte Server verwenden.
Voreingestellte Server
+ %d Chats mit Mitgliedern
+ %d Chat(s)
+ Meldung wurde an die Moderatoren gesendet
+ Sie haben dieses Mitglied übernommen
+ Überprüfung der Mitglieder vor der Aufnahme (\"Anklopfen\").
+ Überprüfung der Mitglieder
+ alle
+ Aus
+ Als Beobachter übernehmen
+ Mitglied übernehmen
+ Chats mit Mitgliedern
+ Chat mit Administratoren
+ Keine Chats mit Mitgliedern
+ Entfernen
+ hat Sie übernommen
+ Chat mit einem Mitglied
+ %d Nachrichten
+ Ein Mitglied wird der Gruppe beitreten. Übernehmen?
+ Ein neues Mitglied will der Gruppe beitreten.
+ Überprüfung
+ Von Administratoren überprüft
+ Aufnahme von Mitgliedern festlegen
+ Speichern der Aufnahme-Einstellungen?
+ Sie können Ihre Meldungen im Chat mit den Administratoren sehen.
+ Chat mit Administratoren
+ %1$s übernommen
+ Als Mitglied übernehmen
+ Fehler beim Übernehmen des Mitglieds
+ Aufnahme von Mitgliedern
+ Ausstehende Überprüfung
+ Chat mit einem Mitglied
+ Übernehmen
+ Bitte warten Sie auf die Überprüfung Ihrer Anfrage durch die Gruppen-Moderatoren, um der Gruppe beitreten zu können.
+ Gruppe wird gelöscht
+ Beitrittsanfrage abgelehnt
+ Von der Gruppe entfernt
+ Sie haben die Gruppe verlassen
+ Kontakt deaktiviert
+ Nicht synchronisiert
+ Fehler beim Löschen des Chats mit dem Mitglied
+ Kontakt nicht bereit
+ Sie können keine Nachrichten senden!
+ Chat löschen
+ Chat mit dem Mitglied löschen?
+ Mitglied ablehnen?
+ Es können keine Nachrichten gesendet werden
+ Kontakt gelöscht
+ Das Mitglied hat eine alte App-Version
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml
index c8897c4063..1a1ca0e8a6 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml
@@ -350,7 +350,7 @@
Az adatbázis titkosítási jelmondata frissülni fog és a beállításokban lesz tárolva.
Adatbázis-azonosító
Adatbázis-azonosító: %d
- Adatbázis-azonosítók és átvitel-izolációs beállítások.
+ Adatbázis-azonosítók és átvitelelkülönítési beállítások.
Az adatbázis titkosítási jelmondata frissülni fog és a Keystore-ban lesz tárolva.
Az adatbázis titkosítva lesz, a jelmondat pedig a beállításokban lesz tárolva.
Kiszolgáló törlése
@@ -366,7 +366,7 @@
%dmp
Kézbesítési jelentések!
Az eszközön nincs beállítva a képernyőzár. A SimpleX-zár az „Adatvédelem és biztonság” menüben kapcsolható be, miután beállította a képernyőzárat az eszközén.
- Titkosítás visszafejtési hiba
+ Titkosításvisszafejtési hiba
Eltűnik: %s
szerkesztve
Törlés
@@ -532,7 +532,7 @@
Fájlok és médiatartalmak
KONZOLHOZ
Nem sikerült a titkosítást újraegyeztetni.
- Hiba történt a felhasználó-profil törlésekor
+ Hiba történt a felhasználóprofil törlésekor
Csoporttag általi javítás nem támogatott
Adja meg az üdvözlőüzenetet…
Titkosított adatbázis
@@ -676,7 +676,7 @@
Csevegési profil létrehozása
Védett a kéretlen tartalommal szemben
Hordozható eszközök leválasztása
- Különböző nevek, profilképek és átvitel-izoláció.
+ Különböző nevek, profilképek és átvitelelkülönítés.
Elutasítás esetén a feladó NEM kap értesítést.
Szerepkörválasztó kibontása
A kép akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később!
@@ -716,7 +716,7 @@
Hamarosan további fejlesztések érkeznek!
A reakciók hozzáadása az üzenetekhez le van tiltva ebben a csevegésben.
Helytelen biztonsági kód!
- Ez akkor fordulhat elő, ha Ön vagy a partnere régi adatbázis biztonsági mentést használt.
+ Ez akkor fordulhat elő, ha Ön vagy a partnere egy régi adatbázis biztonsági mentését használta.
Új számítógép-alkalmazás!
Most már az adminisztrátorok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat (megfigyelő szerepkör)
meghívta őt: %1$s
@@ -810,7 +810,7 @@
%s ajánlotta
Csoport elhagyása
%s összes üzenete meg fog jelenni!
- Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Nem sikerült visszafejteni az üzenetet, mert Ön, vagy a partnere régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült.
+ Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Nem sikerült az üzenetet visszafejteni, mert Ön, vagy a partnere egy régi adatbázis biztonsági mentését használta.\n3. A kapcsolat sérült.
megfigyelő
inkognitó a csoporthivatkozáson keresztül
Onion-kiszolgálók használata, ha azok rendelkezésre állnak.
@@ -1026,7 +1026,7 @@
frissítette a csoport profilját
SIMPLEX CHAT TÁMOGATÁSA
SimpleX Chat szolgáltatás
- Nem lehet üzeneteket küldeni!
+ Ön megfigyelő
%s hitelesítve
Jelszó a megjelenítéshez
Adatvédelem és biztonság
@@ -1203,7 +1203,7 @@
Ön eltávolította őt: %1$s
Jelmondat mentése és a csevegés megnyitása
Menti a beállításokat?
- Nincsenek felhasználó-azonosítók.
+ Nincsenek felhasználói azonosítók.
A közvetlen üzenetek küldése a tagok között le van tiltva.
SOCKS-proxy használata?
Hangszóró kikapcsolva
@@ -1345,7 +1345,7 @@
%1$s nevű csoporthoz.]]>
Amikor az alkalmazás fut
Inkognitóprofilt használ ehhez a csoporthoz – fő profilja megosztásának elkerülése érdekében a meghívók küldése le van tiltva
- Átvitel-izoláció
+ Átvitelelkülönítés
Akkor lesz kapcsolódva, ha a meghívási kérése el lesz fogadva, várjon, vagy ellenőrizze később!
A hangüzenetek küldése le van tiltva.
Alkalmazás akkumulátor-használata /