diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index c6a337036..db37516db 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -7873,7 +7873,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 496; + CURRENT_PROJECT_VERSION = 497; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -7911,7 +7911,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.8.2; + MARKETING_VERSION = 2.9.0; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-Werror=protocol"; "OTHER_SWIFT_FLAGS[arch=*]" = "-D DEBUG"; @@ -7949,7 +7949,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 496; + CURRENT_PROJECT_VERSION = 497; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -7983,7 +7983,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.8.2; + MARKETING_VERSION = 2.9.0; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", diff --git a/Session/Closed Groups/EditGroupViewModel.swift b/Session/Closed Groups/EditGroupViewModel.swift index 745c61a81..909c5bcc9 100644 --- a/Session/Closed Groups/EditGroupViewModel.swift +++ b/Session/Closed Groups/EditGroupViewModel.swift @@ -26,7 +26,7 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl public let editableState: EditableState = EditableState() public let state: TableDataState = TableDataState() public let observableState: ObservableTableSourceState = ObservableTableSourceState() - private let selectedIdsSubject: CurrentValueSubject, Never> = CurrentValueSubject([]) + private let selectedIdsSubject: CurrentValueSubject<(name: String, ids: Set), Never> = CurrentValueSubject(("", [])) private let threadId: String private let userSessionId: SessionId @@ -258,8 +258,8 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl leadingAccessory: .icon(UIImage(named: "icon_invite")?.withRenderingMode(.alwaysTemplate)), title: "membersInvite".localized(), accessibility: Accessibility( - identifier: "Add members", - label: "Add members" + identifier: "Invite button", + label: "Invite button" ), onTap: { [weak self] in self?.inviteContacts(currentGroupName: state.group.name) } ), @@ -315,7 +315,7 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl case (.standard, .failed), (.standard, .notSentYet), (.standard, .pending): return .highlightingBackgroundLabelAndRadio( title: "resend".localized(), - isSelected: selectedIdsSubject.value.contains(memberInfo.profileId), + isSelected: selectedIdsSubject.value.ids.contains(memberInfo.profileId), labelAccessibility: Accessibility( identifier: "Resend invite button", label: "Resend invite button" @@ -328,7 +328,7 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl case (.standard, .accepted), (.zombie, _): return .radio( - isSelected: selectedIdsSubject.value.contains(memberInfo.profileId) + isSelected: selectedIdsSubject.value.ids.contains(memberInfo.profileId) ) } }(), @@ -357,11 +357,17 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl case (.standard, .failed, _), (.standard, .notSentYet, _), (.standard, .pending, _), (.standard, .accepted, _), (.zombie, _, _): - if !selectedIdsSubject.value.contains(memberInfo.profileId) { - selectedIdsSubject.send(selectedIdsSubject.value.inserting(memberInfo.profileId)) + if !selectedIdsSubject.value.ids.contains(memberInfo.profileId) { + selectedIdsSubject.send(( + state.group.name, + selectedIdsSubject.value.ids.inserting(memberInfo.profileId) + )) } else { - selectedIdsSubject.send(selectedIdsSubject.value.removing(memberInfo.profileId)) + selectedIdsSubject.send(( + state.group.name, + selectedIdsSubject.value.ids.removing(memberInfo.profileId) + )) } // Force the table data to be refreshed (the database wouldn't @@ -377,7 +383,7 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl lazy var footerButtonInfo: AnyPublisher = selectedIdsSubject .prepend([]) - .map { selectedIds in + .map { currentGroupName, selectedIds in SessionButton.Info( style: .destructive, title: "remove".localized(), @@ -385,7 +391,7 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl accessibility: Accessibility( identifier: "Remove contact button" ), - onTap: { [weak self] in self?.removeMembers(memberIds: selectedIds) } + onTap: { [weak self] in self?.removeMembers(currentGroupName: currentGroupName, memberIds: selectedIds) } ) } .eraseToAnyPublisher() @@ -423,9 +429,16 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl \(groupMember[.groupId]) = \(threadId) AND \(groupMember[.profileId]) = \(contact[.id]) ) - WHERE \(groupMember[.profileId]) IS NULL + WHERE ( + \(groupMember[.profileId]) IS NULL AND + \(contact[.isApproved]) = TRUE AND + \(contact[.didApproveMe]) = TRUE + ) """), footerTitle: "membersInviteTitle".localized(), + footerAccessibility: Accessibility( + identifier: "Confirm invite button" + ), onSubmit: { [weak self, threadId, dependencies] in switch try? SessionId.Prefix(from: threadId) { case .group: @@ -636,94 +649,144 @@ class EditGroupViewModel: SessionTableViewModel, NavigatableStateHolder, Editabl self.showToast(text: "groupInviteSending".putNumber(1).localized()) } - private func removeMembers(memberIds: Set) { + private func removeMembers(currentGroupName: String, memberIds: Set) { guard !memberIds.isEmpty else { return } - switch try? SessionId.Prefix(from: threadId) { - case .group: - MessageSender - .removeGroupMembers( - groupSessionId: threadId, - memberIds: memberIds, - removeTheirMessages: dependencies[feature: .updatedGroupsRemoveMessagesOnKick], - sendMemberChangedMessage: true, - using: dependencies - ) - .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) - .sinkUntilComplete() - self.selectedIdsSubject.send([]) - - case .standard: // Assume it's a legacy group - let updatedMemberIds: Set = (tableData - .first(where: { $0.model == .members })? - .elements - .compactMap { item -> String? in - switch item.id { - case .member(let profileId): return profileId - default: return nil - } - }) - .defaulting(to: []) - .asSet() - .removing(contentsOf: memberIds) + let memberNames: [String] = memberIds + .compactMap { memberId in + guard + let section: SectionModel = self.tableData + .first(where: { section in section.model == .members }), + let info: SessionCell.Info = section.elements + .first(where: { info in + switch info.id { + case .member(let infoMemberId): return infoMemberId == memberId + default: return false + } + }) + else { + return Profile.truncated(id: memberId, truncating: .middle) + } - let viewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self, dependencies, threadId] modalActivityIndicator in - let currentGroupName: String = dependencies[singleton: .storage] - .read { db in - try ClosedGroup - .filter(id: threadId) - .select(.name) - .asRequest(of: String.self) - .fetchOne(db) - } - .defaulting(to: "groupUnknown".localized()) - - MessageSender - .update( - legacyGroupSessionId: threadId, - with: updatedMemberIds, - name: currentGroupName, - using: dependencies - ) - .eraseToAnyPublisher() - .subscribe(on: DispatchQueue.global(qos: .userInitiated)) - .receive(on: DispatchQueue.main) - .sinkUntilComplete( - receiveCompletion: { [weak self] result in - modalActivityIndicator.dismiss(completion: { - switch result { - case .finished: self?.selectedIdsSubject.send([]) - case .failure: - self?.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "theError".localized(), - body: .text("deleteAfterLegacyGroupsGroupUpdateErrorTitle".localized()), - cancelTitle: "okay".localized(), - cancelStyle: .alert_text + return info.title?.text + } + let confirmationBody: NSAttributedString = { + switch memberNames.count { + case 1: + return "groupRemoveDescription" + .put(key: "name", value: memberNames[0]) + .put(key: "group_name", value: currentGroupName) + .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) + + case 2: + return "groupRemoveDescriptionTwo" + .put(key: "name", value: memberNames[0]) + .put(key: "other_name", value: memberNames[1]) + .put(key: "group_name", value: currentGroupName) + .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) + + default: + return "groupRemoveDescriptionMultiple" + .put(key: "name", value: memberNames[0]) + .put(key: "count", value: memberNames.count - 1) + .put(key: "group_name", value: currentGroupName) + .localizedFormatted(baseFont: .systemFont(ofSize: Values.smallFontSize)) + } + }() + let confirmationModal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "remove".localized(), + body: .attributedText(confirmationBody), + confirmTitle: "remove".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text, + dismissOnConfirm: false, + onConfirm: { [weak self, threadId, dependencies] modal in + switch try? SessionId.Prefix(from: threadId) { + case .group: + MessageSender + .removeGroupMembers( + groupSessionId: threadId, + memberIds: memberIds, + removeTheirMessages: dependencies[feature: .updatedGroupsRemoveMessagesOnKick], + sendMemberChangedMessage: true, + using: dependencies + ) + .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) + .sinkUntilComplete() + self?.selectedIdsSubject.send((currentGroupName, [])) + modal.dismiss(animated: true) + + case .standard: // Assume it's a legacy group + let updatedMemberIds: Set = (self?.tableData + .first(where: { $0.model == .members })? + .elements + .compactMap { item -> String? in + switch item.id { + case .member(let profileId): return profileId + default: return nil + } + }) + .defaulting(to: []) + .asSet() + .removing(contentsOf: memberIds) + + let viewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self, dependencies, threadId] modalActivityIndicator in + MessageSender + .update( + legacyGroupSessionId: threadId, + with: updatedMemberIds, + name: currentGroupName, + using: dependencies + ) + .eraseToAnyPublisher() + .subscribe(on: DispatchQueue.global(qos: .userInitiated)) + .receive(on: DispatchQueue.main) + .sinkUntilComplete( + receiveCompletion: { [weak self] result in + modalActivityIndicator.dismiss(completion: { + switch result { + case .finished: + self?.selectedIdsSubject.send((currentGroupName, [])) + modalActivityIndicator.dismiss { + modal.dismiss(animated: true) + } + + case .failure: + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "theError".localized(), + body: .text("deleteAfterLegacyGroupsGroupUpdateErrorTitle".localized()), + cancelTitle: "okay".localized(), + cancelStyle: .alert_text + ) + ), + transitionType: .present ) - ), - transitionType: .present - ) + } + }) } - }) - } + ) + } + self?.transitionToScreen(viewController, transitionType: .present) + + default: + self?.transitionToScreen( + ConfirmationModal( + info: ConfirmationModal.Info( + title: "theError".localized(), + body: .text("deleteAfterLegacyGroupsGroupUpdateErrorTitle".localized()), + cancelTitle: "okay".localized(), + cancelStyle: .alert_text + ) + ), + transitionType: .present ) + } } - self.transitionToScreen(viewController, transitionType: .present) - - default: - self.transitionToScreen( - ConfirmationModal( - info: ConfirmationModal.Info( - title: "theError".localized(), - body: .text("deleteAfterLegacyGroupsGroupUpdateErrorTitle".localized()), - cancelTitle: "okay".localized(), - cancelStyle: .alert_text - ) - ), - transitionType: .present - ) - } + ) + ) + self.transitionToScreen(confirmationModal, transitionType: .present) } } diff --git a/Session/Closed Groups/NewClosedGroupVC.swift b/Session/Closed Groups/NewClosedGroupVC.swift index 78d711cf8..31d2086e5 100644 --- a/Session/Closed Groups/NewClosedGroupVC.swift +++ b/Session/Closed Groups/NewClosedGroupVC.swift @@ -74,7 +74,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate accessibility: Accessibility(label: "Version warning banner") ) ) - result.isHidden = dependencies[feature: .updatedGroups] + result.isHidden = !dependencies[feature: .updatedGroups] return result }() @@ -100,6 +100,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate result.themeTintColor = .textPrimary result.themeBackgroundColor = .clear result.delegate = self + result.searchTextField.accessibilityIdentifier = "Search contacts field" result.set(.height, to: NewClosedGroupVC.searchBarHeight) return result diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 1ad2e46da..b3458b029 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -929,7 +929,15 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob title: "groupMembers".localized(), showProfileIcons: true, request: GroupMember - .filter(GroupMember.Columns.groupId == threadId), + .select( + GroupMember.Columns.groupId, + GroupMember.Columns.profileId, + max(GroupMember.Columns.role).forKey(GroupMember.Columns.role.name), + GroupMember.Columns.roleStatus, + GroupMember.Columns.isHidden + ) + .filter(GroupMember.Columns.groupId == threadId) + .group(GroupMember.Columns.profileId), onTap: .callback { [weak self, dependencies] _, memberInfo in dependencies[singleton: .storage].write { db in try SessionThread.fetchOrCreate( diff --git a/Session/Shared/UserListViewModel.swift b/Session/Shared/UserListViewModel.swift index a3a63ec95..3fbe3ada8 100644 --- a/Session/Shared/UserListViewModel.swift +++ b/Session/Shared/UserListViewModel.swift @@ -23,6 +23,7 @@ class UserListViewModel: SessionTableVie private let showProfileIcons: Bool private let request: (any FetchRequest) private let footerTitle: String? + private let footerAccessibility: Accessibility? private let onTapAction: OnTapAction private let onSubmitAction: OnSubmitAction @@ -35,6 +36,7 @@ class UserListViewModel: SessionTableVie showProfileIcons: Bool, request: (any FetchRequest), footerTitle: String? = nil, + footerAccessibility: Accessibility? = nil, onTap: OnTapAction = .radio, onSubmit: OnSubmitAction = .none, using dependencies: Dependencies @@ -46,6 +48,7 @@ class UserListViewModel: SessionTableVie self.showProfileIcons = showProfileIcons self.request = request self.footerTitle = footerTitle + self.footerAccessibility = footerAccessibility self.onTapAction = onTap self.onSubmitAction = onSubmit } @@ -189,13 +192,14 @@ class UserListViewModel: SessionTableVie lazy var footerButtonInfo: AnyPublisher = selectedUsersSubject .prepend([]) - .map { [weak self, dependencies, footerTitle] selectedUsers -> SessionButton.Info? in + .map { [weak self, dependencies, footerTitle, footerAccessibility] selectedUsers -> SessionButton.Info? in guard self?.onSubmitAction.hasAction == true, let title: String = footerTitle else { return nil } return SessionButton.Info( style: .bordered, title: title, isEnabled: !selectedUsers.isEmpty, + accessibility: footerAccessibility, onTap: { self?.submit(with: selectedUsers) } ) } diff --git a/Session/Shared/Views/SessionCell+AccessoryView.swift b/Session/Shared/Views/SessionCell+AccessoryView.swift index 9247fe7f3..ae67c78d7 100644 --- a/Session/Shared/Views/SessionCell+AccessoryView.swift +++ b/Session/Shared/Views/SessionCell+AccessoryView.swift @@ -464,6 +464,7 @@ extension SessionCell { // MARK: -- HighlightingBackgroundLabel case let accessory as SessionCell.AccessoryConfig.HighlightingBackgroundLabel: + highlightingBackgroundLabel.isAccessibilityElement = (accessory.accessibility != nil) highlightingBackgroundLabel.accessibilityIdentifier = accessory.accessibility?.identifier highlightingBackgroundLabel.accessibilityLabel = accessory.accessibility?.label highlightingBackgroundLabel.text = accessory.title @@ -476,9 +477,13 @@ extension SessionCell { case let accessory as SessionCell.AccessoryConfig.HighlightingBackgroundLabelAndRadio: let isSelected: Bool = accessory.liveIsSelected() let wasOldSelection: Bool = (!isSelected && accessory.wasSavedSelection) + highlightingBackgroundLabel.isAccessibilityElement = (accessory.labelAccessibility != nil) highlightingBackgroundLabel.accessibilityIdentifier = accessory.labelAccessibility?.identifier highlightingBackgroundLabel.accessibilityLabel = accessory.labelAccessibility?.label - radioView.isAccessibilityElement = true + + radioBorderView.isAccessibilityElement = true + radioBorderView.accessibilityIdentifier = accessory.accessibility?.identifier + radioBorderView.accessibilityLabel = accessory.accessibility?.label if isSelected || wasOldSelection { radioView.accessibilityTraits.insert(.selected) @@ -503,8 +508,6 @@ extension SessionCell { radioBorderView.layer.cornerRadius = (accessory.size.borderSize / 2) - radioView.accessibilityIdentifier = accessory.accessibility?.identifier - radioView.accessibilityLabel = accessory.accessibility?.label radioView.alpha = (wasOldSelection ? 0.3 : 1) radioView.isHidden = (!isSelected && !accessory.wasSavedSelection) radioView.themeBackgroundColor = { diff --git a/SessionMessagingKit/Database/Models/GroupMember.swift b/SessionMessagingKit/Database/Models/GroupMember.swift index b283d8bbb..3f84ce33f 100644 --- a/SessionMessagingKit/Database/Models/GroupMember.swift +++ b/SessionMessagingKit/Database/Models/GroupMember.swift @@ -1,6 +1,4 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -// -// stringlint:disable import Foundation import GRDB diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift index c881ea188..d358ea3a0 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift @@ -213,8 +213,8 @@ internal extension LibSessionCacheType { .filter(SessionThread.Columns.variant == SessionThread.Variant.contact) .filter( /// Only want to include include standard contact conversations (not blinded conversations) - ClosedGroup.Columns.threadId > SessionId.Prefix.standard.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.standard.endOfRangeString + SessionThread.Columns.id > SessionId.Prefix.standard.rawValue && + SessionThread.Columns.id < SessionId.Prefix.standard.endOfRangeString ) .select(.id) .asRequest(of: String.self) diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index 9ae68c16c..46e9fa563 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -171,7 +171,10 @@ public extension LibSession { let missingRequiredVariants: Set = ConfigDump.Variant.userVariants .subtracting(existingDumpVariants) let groupsByKey: [String: ClosedGroup] = (try? ClosedGroup - .filter(ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%")) + .filter( + ClosedGroup.Columns.threadId > SessionId.Prefix.group.rawValue && + ClosedGroup.Columns.threadId < SessionId.Prefix.group.endOfRangeString + ) .fetchAll(db) .reduce(into: [:]) { result, next in result[next.threadId] = next }) .defaulting(to: [:]) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index d2f10978a..bc2a7686e 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -57,12 +57,12 @@ extension MessageReceiver { .filter(SessionThread.Columns.variant == SessionThread.Variant.contact) .filter( ( - ClosedGroup.Columns.threadId > SessionId.Prefix.blinded15.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.blinded15.endOfRangeString + SessionThread.Columns.id > SessionId.Prefix.blinded15.rawValue && + SessionThread.Columns.id < SessionId.Prefix.blinded15.endOfRangeString ) || ( - ClosedGroup.Columns.threadId > SessionId.Prefix.blinded25.rawValue && - ClosedGroup.Columns.threadId < SessionId.Prefix.blinded25.endOfRangeString + SessionThread.Columns.id > SessionId.Prefix.blinded25.rawValue && + SessionThread.Columns.id < SessionId.Prefix.blinded25.endOfRangeString ) ) .asRequest(of: String.self)