diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index e4d2a8318..24a77cb3d 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1141,6 +1141,12 @@ func apiMemberRole(_ groupId: Int64, _ memberId: Int64, _ memberRole: GroupMembe throw r } +func apiBlockMemberForAll(_ groupId: Int64, _ memberId: Int64, _ blocked: Bool) async throws -> GroupMember { + let r = await chatSendCmd(.apiBlockMemberForAll(groupId: groupId, memberId: memberId, blocked: blocked), bgTask: false) + if case let .memberBlockedForAllUser(_, _, member, _) = r { return member } + throw r +} + func leaveGroup(_ groupId: Int64) async { do { let groupInfo = try await apiLeaveGroup(groupId) @@ -1680,6 +1686,13 @@ func processReceivedMsg(_ res: ChatResponse) async { _ = m.upsertGroupMember(groupInfo, member) } } + case let .memberBlockedForAll(user, groupInfo, byMember: _, member: member, blocked: _): + if active(user) { + await MainActor.run { + m.updateGroup(groupInfo) + _ = m.upsertGroupMember(groupInfo, member) + } + } case let .newMemberContactReceivedInv(user, contact, _, _): if active(user) { await MainActor.run { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 1f0ab9ca4..f7775a7cd 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -46,7 +46,9 @@ struct FramedItemView: View { framedItemHeader(icon: "flag", caption: Text("moderated by \(byGroupMember.displayName)").italic()) case .blocked: framedItemHeader(icon: "hand.raised", caption: Text("blocked").italic()) - default: + case .blockedByAdmin: + framedItemHeader(icon: "hand.raised", caption: Text("blocked by admin").italic()) + case .deleted: framedItemHeader(icon: "trash", caption: Text("marked deleted").italic()) } } else if chatItem.meta.isLive { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift index c6af95e6f..dfa4a97fc 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift @@ -33,6 +33,7 @@ struct MarkedDeletedItemView: View { var i = m.getChatItemIndex(chatItem) { var moderated = 0 var blocked = 0 + var blockedByAdmin = 0 var deleted = 0 var moderatedBy: Set = [] while i < m.reversedChatItems.count, @@ -44,16 +45,19 @@ struct MarkedDeletedItemView: View { moderated += 1 moderatedBy.insert(byGroupMember.displayName) case .blocked: blocked += 1 + case .blockedByAdmin: blockedByAdmin += 1 case .deleted: deleted += 1 } i += 1 } - let total = moderated + blocked + deleted + let total = moderated + blocked + blockedByAdmin + deleted return total <= 1 ? markedDeletedText : total == moderated ? "\(total) messages moderated by \(moderatedBy.joined(separator: ", "))" - : total == blocked + : total == blockedByAdmin + ? "\(total) messages blocked by admin" + : total == blocked + blockedByAdmin ? "\(total) messages blocked" : "\(total) messages marked deleted" } else { @@ -65,7 +69,8 @@ struct MarkedDeletedItemView: View { switch chatItem.meta.itemDeleted { case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)" case .blocked: "blocked" - default: "marked deleted" + case .blockedByAdmin: "blocked by admin" + case .deleted, nil: "marked deleted" } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index 657df6065..8f67a8f73 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -109,6 +109,7 @@ struct ChatItemContentView: View { case let .rcvGroupFeatureRejected(feature): chatFeatureView(feature, .red) case .sndModerated: deletedItemView() case .rcvModerated: deletedItemView() + case .rcvBlocked: deletedItemView() case let .invalidJSON(json): CIInvalidJSONView(json: json) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index e53347b74..3879e78d3 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -36,6 +36,8 @@ struct GroupChatInfoView: View { case largeGroupReceiptsDisabled case blockMemberAlert(mem: GroupMember) case unblockMemberAlert(mem: GroupMember) + case blockForAllAlert(mem: GroupMember) + case unblockForAllAlert(mem: GroupMember) case removeMemberAlert(mem: GroupMember) case error(title: LocalizedStringKey, error: LocalizedStringKey) @@ -48,6 +50,8 @@ struct GroupChatInfoView: View { case .largeGroupReceiptsDisabled: return "largeGroupReceiptsDisabled" case let .blockMemberAlert(mem): return "blockMemberAlert \(mem.groupMemberId)" case let .unblockMemberAlert(mem): return "unblockMemberAlert \(mem.groupMemberId)" + case let .blockForAllAlert(mem): return "blockForAllAlert \(mem.groupMemberId)" + case let .unblockForAllAlert(mem): return "unblockForAllAlert \(mem.groupMemberId)" case let .removeMemberAlert(mem): return "removeMemberAlert \(mem.groupMemberId)" case let .error(title, _): return "error \(title)" } @@ -143,6 +147,8 @@ struct GroupChatInfoView: View { case .largeGroupReceiptsDisabled: return largeGroupReceiptsDisabledAlert() case let .blockMemberAlert(mem): return blockMemberAlert(groupInfo, mem) case let .unblockMemberAlert(mem): return unblockMemberAlert(groupInfo, mem) + case let .blockForAllAlert(mem): return blockForAllAlert(groupInfo, mem) + case let .unblockForAllAlert(mem): return unblockForAllAlert(groupInfo, mem) case let .removeMemberAlert(mem): return removeMemberAlert(mem) case let .error(title, error): return Alert(title: Text(title), message: Text(error)) } @@ -226,13 +232,10 @@ struct GroupChatInfoView: View { .foregroundColor(.secondary) } Spacer() - let role = member.memberRole - if [.owner, .admin, .observer].contains(role) { - Text(member.memberRole.text) - .foregroundColor(.secondary) - } + memberInfo(member) } + // revert from this: if user { v } else if member.canBeRemoved(groupInfo: groupInfo) { @@ -240,6 +243,43 @@ struct GroupChatInfoView: View { } else { blockSwipe(member, v) } + // revert to this: vvv +// if user { +// v +// } else if groupInfo.membership.memberRole >= .admin { +// // TODO if there are more actions, refactor with lists of swipeActions +// let canBlockForAll = member.canBlockForAll(groupInfo: groupInfo) +// let canRemove = member.canBeRemoved(groupInfo: groupInfo) +// if canBlockForAll && canRemove { +// removeSwipe(member, blockForAllSwipe(member, v)) +// } else if canBlockForAll { +// blockForAllSwipe(member, v) +// } else if canRemove { +// removeSwipe(member, v) +// } else { +// v +// } +// } else { +// if !member.blockedByAdmin { +// blockSwipe(member, v) +// } else { +// v +// } +// } + // ^^^ + } + + @ViewBuilder private func memberInfo(_ member: GroupMember) -> some View { + if member.blocked { + Text("blocked") + .foregroundColor(.secondary) + } else { + let role = member.memberRole + if [.owner, .admin, .observer].contains(role) { + Text(member.memberRole.text) + .foregroundColor(.secondary) + } + } } private func blockSwipe(_ member: GroupMember, _ v: V) -> some View { @@ -260,6 +300,24 @@ struct GroupChatInfoView: View { } } + private func blockForAllSwipe(_ member: GroupMember, _ v: V) -> some View { + v.swipeActions(edge: .leading) { + if member.blockedByAdmin { + Button { + alert = .unblockForAllAlert(mem: member) + } label: { + Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(.accentColor) + } + } else { + Button { + alert = .blockForAllAlert(mem: member) + } label: { + Label("Block for all", systemImage: "hand.raised").foregroundColor(.secondary) + } + } + } + } + private func removeSwipe(_ member: GroupMember, _ v: V) -> some View { v.swipeActions(edge: .trailing) { Button(role: .destructive) { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 7e336c332..d2b0f7739 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -27,6 +27,8 @@ struct GroupMemberInfoView: View { enum GroupMemberInfoViewAlert: Identifiable { case blockMemberAlert(mem: GroupMember) case unblockMemberAlert(mem: GroupMember) + case blockForAllAlert(mem: GroupMember) + case unblockForAllAlert(mem: GroupMember) case removeMemberAlert(mem: GroupMember) case changeMemberRoleAlert(mem: GroupMember, role: GroupMemberRole) case switchAddressAlert @@ -39,6 +41,8 @@ struct GroupMemberInfoView: View { switch self { case let .blockMemberAlert(mem): return "blockMemberAlert \(mem.groupMemberId)" case let .unblockMemberAlert(mem): return "unblockMemberAlert \(mem.groupMemberId)" + case let .blockForAllAlert(mem): return "blockForAllAlert \(mem.groupMemberId)" + case let .unblockForAllAlert(mem): return "unblockForAllAlert \(mem.groupMemberId)" case let .removeMemberAlert(mem): return "removeMemberAlert \(mem.groupMemberId)" case let .changeMemberRoleAlert(mem, role): return "changeMemberRoleAlert \(mem.groupMemberId) \(role.rawValue)" case .switchAddressAlert: return "switchAddressAlert" @@ -164,6 +168,7 @@ struct GroupMemberInfoView: View { } } + // revert from this: Section { if member.memberSettings.showMessages { blockMemberButton(member) @@ -171,9 +176,16 @@ struct GroupMemberInfoView: View { unblockMemberButton(member) } if member.canBeRemoved(groupInfo: groupInfo) { - removeMemberButton(member) + removeMemberButton(member) } } + // revert to this: vvv +// if groupInfo.membership.memberRole >= .admin { +// adminDestructiveSection(member) +// } else { +// nonAdminBlockSection(member) +// } + // ^^^ if developerTools { Section("For console") { @@ -216,6 +228,8 @@ struct GroupMemberInfoView: View { switch(alertItem) { case let .blockMemberAlert(mem): return blockMemberAlert(groupInfo, mem) case let .unblockMemberAlert(mem): return unblockMemberAlert(groupInfo, mem) + case let .blockForAllAlert(mem): return blockForAllAlert(groupInfo, mem) + case let .unblockForAllAlert(mem): return unblockForAllAlert(groupInfo, mem) case let .removeMemberAlert(mem): return removeMemberAlert(mem) case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem) case .switchAddressAlert: return switchAddressAlert(switchMemberAddress) @@ -385,6 +399,55 @@ struct GroupMemberInfoView: View { } } + @ViewBuilder private func adminDestructiveSection(_ mem: GroupMember) -> some View { + let canBlockForAll = mem.canBlockForAll(groupInfo: groupInfo) + let canRemove = mem.canBeRemoved(groupInfo: groupInfo) + if canBlockForAll || canRemove { + Section { + if canBlockForAll { + if mem.blockedByAdmin { + unblockForAllButton(mem) + } else { + blockForAllButton(mem) + } + } + if canRemove { + removeMemberButton(mem) + } + } + } + } + + private func nonAdminBlockSection(_ mem: GroupMember) -> some View { + Section { + if mem.blockedByAdmin { + Label("Blocked by admin", systemImage: "hand.raised") + .foregroundColor(.secondary) + } else if mem.memberSettings.showMessages { + blockMemberButton(mem) + } else { + unblockMemberButton(mem) + } + } + } + + private func blockForAllButton(_ mem: GroupMember) -> some View { + Button(role: .destructive) { + alert = .blockForAllAlert(mem: mem) + } label: { + Label("Block for all", systemImage: "hand.raised") + .foregroundColor(.red) + } + } + + private func unblockForAllButton(_ mem: GroupMember) -> some View { + Button { + alert = .unblockForAllAlert(mem: mem) + } label: { + Label("Unblock for all", systemImage: "hand.raised.slash") + } + } + private func blockMemberButton(_ mem: GroupMember) -> some View { Button(role: .destructive) { alert = .blockMemberAlert(mem: mem) @@ -560,6 +623,41 @@ func updateMemberSettings(_ gInfo: GroupInfo, _ member: GroupMember, _ memberSet } } +func blockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert { + Alert( + title: Text("Block member for all?"), + message: Text("All new messages from \(mem.chatViewName) will be hidden!"), + primaryButton: .destructive(Text("Block for all")) { + blockMemberForAll(gInfo, mem, true) + }, + secondaryButton: .cancel() + ) +} + +func unblockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert { + Alert( + title: Text("Unblock member for all?"), + message: Text("Messages from \(mem.chatViewName) will be shown!"), + primaryButton: .default(Text("Unblock for all")) { + blockMemberForAll(gInfo, mem, false) + }, + secondaryButton: .cancel() + ) +} + +func blockMemberForAll(_ gInfo: GroupInfo, _ member: GroupMember, _ blocked: Bool) { + Task { + do { + let updatedMember = try await apiBlockMemberForAll(gInfo.groupId, member.groupMemberId, blocked) + await MainActor.run { + _ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember) + } + } catch let error { + logger.error("apiBlockMemberForAll error: \(responseError(error))") + } + } +} + struct GroupMemberInfoView_Previews: PreviewProvider { static var previews: some View { GroupMemberInfoView( diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index aff0d0dc3..ae091f841 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -55,6 +55,7 @@ public enum ChatCommand { case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole) case apiJoinGroup(groupId: Int64) case apiMemberRole(groupId: Int64, memberId: Int64, memberRole: GroupMemberRole) + case apiBlockMemberForAll(groupId: Int64, memberId: Int64, blocked: Bool) case apiRemoveMember(groupId: Int64, memberId: Int64) case apiLeaveGroup(groupId: Int64) case apiListMembers(groupId: Int64) @@ -195,6 +196,7 @@ public enum ChatCommand { case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)" case let .apiJoinGroup(groupId): return "/_join #\(groupId)" case let .apiMemberRole(groupId, memberId, memberRole): return "/_member role #\(groupId) \(memberId) \(memberRole.rawValue)" + case let .apiBlockMemberForAll(groupId, memberId, blocked): return "/_block #\(groupId) \(memberId) blocked=\(onOff(blocked))" case let .apiRemoveMember(groupId, memberId): return "/_remove #\(groupId) \(memberId)" case let .apiLeaveGroup(groupId): return "/_leave #\(groupId)" case let .apiListMembers(groupId): return "/_members #\(groupId)" @@ -334,6 +336,7 @@ public enum ChatCommand { case .apiAddMember: return "apiAddMember" case .apiJoinGroup: return "apiJoinGroup" case .apiMemberRole: return "apiMemberRole" + case .apiBlockMemberForAll: return "apiBlockMemberForAll" case .apiRemoveMember: return "apiRemoveMember" case .apiLeaveGroup: return "apiLeaveGroup" case .apiListMembers: return "apiListMembers" @@ -566,6 +569,8 @@ public enum ChatResponse: Decodable, Error { case joinedGroupMemberConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, member: GroupMember) case memberRole(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole) case memberRoleUser(user: UserRef, groupInfo: GroupInfo, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole) + case memberBlockedForAll(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, blocked: Bool) + case memberBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, member: GroupMember, blocked: Bool) case deletedMemberUser(user: UserRef, groupInfo: GroupInfo, member: GroupMember) case deletedMember(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, deletedMember: GroupMember) case leftMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember) @@ -716,6 +721,8 @@ public enum ChatResponse: Decodable, Error { case .joinedGroupMemberConnecting: return "joinedGroupMemberConnecting" case .memberRole: return "memberRole" case .memberRoleUser: return "memberRoleUser" + case .memberBlockedForAll: return "memberBlockedForAll" + case .memberBlockedForAllUser: return "memberBlockedForAllUser" case .deletedMemberUser: return "deletedMemberUser" case .deletedMember: return "deletedMember" case .leftMember: return "leftMember" @@ -864,6 +871,8 @@ public enum ChatResponse: Decodable, Error { case let .joinedGroupMemberConnecting(u, groupInfo, hostMember, member): return withUser(u, "groupInfo: \(groupInfo)\nhostMember: \(hostMember)\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 .memberRoleUser(u, groupInfo, member, fromRole, toRole): return withUser(u, "groupInfo: \(groupInfo)\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 .memberBlockedForAllUser(u, groupInfo, member, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nblocked: \(blocked)") case let .deletedMemberUser(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") case let .deletedMember(u, groupInfo, byMember, deletedMember): return withUser(u, "groupInfo: \(groupInfo)\nbyMember: \(byMember)\ndeletedMember: \(deletedMember)") case let .leftMember(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)") diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 5426596a4..ff61a51d3 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1814,6 +1814,7 @@ public struct GroupMember: Identifiable, Decodable { public var memberCategory: GroupMemberCategory public var memberStatus: GroupMemberStatus public var memberSettings: GroupMemberSettings + public var blockedByAdmin: Bool public var invitedBy: InvitedBy public var localDisplayName: ContactName public var memberProfile: LocalProfile @@ -1833,6 +1834,7 @@ public struct GroupMember: Identifiable, Decodable { public var image: String? { get { memberProfile.image } } public var contactLink: String? { get { memberProfile.contactLink } } public var verified: Bool { activeConn?.connectionCode != nil } + public var blocked: Bool { blockedByAdmin || !memberSettings.showMessages } var directChatId: ChatId? { get { @@ -1899,7 +1901,7 @@ public struct GroupMember: Identifiable, Decodable { public func canBeRemoved(groupInfo: GroupInfo) -> Bool { let userRole = groupInfo.membership.memberRole return memberStatus != .memRemoved && memberStatus != .memLeft - && userRole >= .admin && userRole >= memberRole && groupInfo.membership.memberCurrent + && userRole >= .admin && userRole >= memberRole && groupInfo.membership.memberActive } public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? { @@ -1908,6 +1910,12 @@ public struct GroupMember: Identifiable, Decodable { return GroupMemberRole.allCases.filter { $0 <= userRole && $0 != .author } } + public func canBlockForAll(groupInfo: GroupInfo) -> Bool { + let userRole = groupInfo.membership.memberRole + return memberStatus != .memRemoved && memberStatus != .memLeft && memberRole < .admin + && userRole >= .admin && userRole >= memberRole && groupInfo.membership.memberActive + } + public var memberIncognito: Bool { memberProfile.profileId != memberContactProfileId } @@ -1920,6 +1928,7 @@ public struct GroupMember: Identifiable, Decodable { memberCategory: .inviteeMember, memberStatus: .memComplete, memberSettings: GroupMemberSettings(showMessages: true), + blockedByAdmin: false, invitedBy: .user, localDisplayName: "alice", memberProfile: LocalProfile.sampleData, @@ -2184,6 +2193,7 @@ public struct ChatItem: Identifiable, Decodable { case .rcvDeleted: return true case .sndModerated: return true case .rcvModerated: return true + case .rcvBlocked: return true default: return false } } @@ -2228,22 +2238,18 @@ public struct ChatItem: Identifiable, Decodable { } } - private var showNtfDir: Bool { - return !chatDir.sent - } - public var showNotification: Bool { switch content { - case .sndMsgContent: return showNtfDir - case .rcvMsgContent: return showNtfDir - case .sndDeleted: return showNtfDir - case .rcvDeleted: return showNtfDir - case .sndCall: return showNtfDir + case .sndMsgContent: return false + case .rcvMsgContent: return meta.itemDeleted == nil + case .sndDeleted: return false + case .rcvDeleted: return false + case .sndCall: return false case .rcvCall: return false // notification is shown on .callInvitation instead - case .rcvIntegrityError: return showNtfDir - case .rcvDecryptionError: return showNtfDir - case .rcvGroupInvitation: return showNtfDir - case .sndGroupInvitation: return showNtfDir + case .rcvIntegrityError: return false + case .rcvDecryptionError: return false + case .rcvGroupInvitation: return true + case .sndGroupInvitation: return false case .rcvDirectEvent(rcvDirectEvent: let rcvDirectEvent): switch rcvDirectEvent { case .contactDeleted: return false @@ -2254,9 +2260,10 @@ public struct ChatItem: Identifiable, Decodable { case .groupUpdated: return false case .memberConnected: return false case .memberRole: return false - case .userRole: return showNtfDir - case .userDeleted: return showNtfDir - case .groupDeleted: return showNtfDir + case .memberBlocked: return false + case .userRole: return true + case .userDeleted: return true + case .groupDeleted: return true case .memberAdded: return false case .memberLeft: return false case .memberDeleted: return false @@ -2264,19 +2271,20 @@ public struct ChatItem: Identifiable, Decodable { case .memberCreatedContact: return false case .memberProfileUpdated: return false } - case .sndGroupEvent: return showNtfDir + case .sndGroupEvent: return false case .rcvConnEvent: return false - case .sndConnEvent: return showNtfDir + case .sndConnEvent: return false case .rcvChatFeature: return false - case .sndChatFeature: return showNtfDir + case .sndChatFeature: return false case .rcvChatPreference: return false - case .sndChatPreference: return showNtfDir + case .sndChatPreference: return false case .rcvGroupFeature: return false - case .sndGroupFeature: return showNtfDir - case .rcvChatFeatureRejected: return showNtfDir - case .rcvGroupFeatureRejected: return showNtfDir - case .sndModerated: return true - case .rcvModerated: return true + case .sndGroupFeature: return false + case .rcvChatFeatureRejected: return true + case .rcvGroupFeatureRejected: return false + case .sndModerated: return false + case .rcvModerated: return false + case .rcvBlocked: return false case .invalidJSON: return false } } @@ -2663,12 +2671,14 @@ public enum SndCIStatusProgress: String, Decodable { public enum CIDeleted: Decodable { case deleted(deletedTs: Date?) case blocked(deletedTs: Date?) + case blockedByAdmin(deletedTs: Date?) case moderated(deletedTs: Date?, byGroupMember: GroupMember) var id: String { switch self { case .deleted: return "deleted" case .blocked: return "blocked" + case .blockedByAdmin: return "blocked by admin" case .moderated: return "moderated" } } @@ -2709,6 +2719,7 @@ public enum CIContent: Decodable, ItemContent { case rcvGroupFeatureRejected(groupFeature: GroupFeature) case sndModerated case rcvModerated + case rcvBlocked case invalidJSON(json: String) public var text: String { @@ -2739,6 +2750,7 @@ public enum CIContent: Decodable, ItemContent { case let .rcvGroupFeatureRejected(groupFeature): return String.localizedStringWithFormat("%@: received, prohibited", groupFeature.text) case .sndModerated: return NSLocalizedString("moderated", comment: "moderated chat item") case .rcvModerated: return NSLocalizedString("moderated", comment: "moderated chat item") + case .rcvBlocked: return NSLocalizedString("blocked by admin", comment: "blocked chat item") case .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item") } } @@ -2777,6 +2789,7 @@ public enum CIContent: Decodable, ItemContent { case .rcvDecryptionError: return true case .rcvGroupInvitation: return true case .rcvModerated: return true + case .rcvBlocked: return true case .invalidJSON: return true default: return false } @@ -3463,6 +3476,7 @@ public enum RcvGroupEvent: Decodable { case memberConnected case memberLeft case memberRole(groupMemberId: Int64, profile: Profile, role: GroupMemberRole) + case memberBlocked(groupMemberId: Int64, profile: Profile, blocked: Bool) case userRole(role: GroupMemberRole) case memberDeleted(groupMemberId: Int64, profile: Profile) case userDeleted @@ -3480,6 +3494,12 @@ public enum RcvGroupEvent: Decodable { 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) + case let .memberBlocked(_, profile, blocked): + if blocked { + return String.localizedStringWithFormat(NSLocalizedString("blocked %@", comment: "rcv group event chat item"), profile.profileViewName) + } else { + return String.localizedStringWithFormat(NSLocalizedString("unblocked %@", comment: "rcv group event chat item"), profile.profileViewName) + } case let .userRole(role): return String.localizedStringWithFormat(NSLocalizedString("changed your role to %@", comment: "rcv group event chat item"), role.text) case let .memberDeleted(_, profile): @@ -3510,6 +3530,7 @@ public enum RcvGroupEvent: Decodable { public enum SndGroupEvent: Decodable { case memberRole(groupMemberId: Int64, profile: Profile, role: GroupMemberRole) case userRole(role: GroupMemberRole) + case memberBlocked(groupMemberId: Int64, profile: Profile, blocked: Bool) case memberDeleted(groupMemberId: Int64, profile: Profile) case userLeft case groupUpdated(groupProfile: GroupProfile) @@ -3520,6 +3541,12 @@ public enum SndGroupEvent: Decodable { return String.localizedStringWithFormat(NSLocalizedString("you changed role of %@ to %@", comment: "snd group event chat item"), profile.profileViewName, role.text) case let .userRole(role): return String.localizedStringWithFormat(NSLocalizedString("you changed role for yourself to %@", comment: "snd group event chat item"), role.text) + case let .memberBlocked(_, profile, blocked): + if blocked { + return String.localizedStringWithFormat(NSLocalizedString("you blocked %@", comment: "snd group event chat item"), profile.profileViewName) + } else { + return String.localizedStringWithFormat(NSLocalizedString("you unblocked %@", comment: "snd group event chat item"), profile.profileViewName) + } case let .memberDeleted(_, profile): 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")