From 529cb2e0f9194d1ecdfd7484c24497b2e764267f Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 4 Feb 2025 12:19:56 +1100 Subject: [PATCH] Added the legacy group deprecation logic (pending final copy) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added a separate feature flag for deprecating legacy groups • Added the ability for a legacy group admin to "Recreate" the group (ie. pre-fill a new group with the same members) • Updated the "Leave" button to be "Delete" for legacy groups once deprecated • Disabled the "mark as unread" action for legacy groups once deprecated • Disabled the input and conversation settings for legacy groups once deprecated • Disabled the long press menu and removing reactions for legacy groups once deprecated --- Scripts/LintLocalizableStrings.swift | 4 +- Session/Closed Groups/NewClosedGroupVC.swift | 44 ++++++--- .../ConversationVC+Interaction.swift | 96 +++++++++++++++++++ Session/Conversations/ConversationVC.swift | 94 +++++++++++++++--- .../Conversations/ConversationViewModel.swift | 21 ++++ Session/Home/HomeVC.swift | 28 ++++-- .../Settings/DeveloperSettingsViewModel.swift | 35 ++++++- Session/Shared/BaseVC.swift | 22 +++++ .../Pollers/GroupPoller.swift | 6 ++ .../SessionThreadViewModel.swift | 11 +++ SessionUtilitiesKit/General/Feature.swift | 8 ++ 11 files changed, 333 insertions(+), 36 deletions(-) diff --git a/Scripts/LintLocalizableStrings.swift b/Scripts/LintLocalizableStrings.swift index 5b69e371a..c13e8f970 100755 --- a/Scripts/LintLocalizableStrings.swift +++ b/Scripts/LintLocalizableStrings.swift @@ -134,7 +134,8 @@ extension ProjectState { .regex(Regex.databaseTableName), .regex(Regex.enumCaseDefinition), .regex(Regex.imageInitialization), - .regex(Regex.variableToStringConversion) + .regex(Regex.variableToStringConversion), + .regex(Regex.localizedParameter) ] } @@ -324,6 +325,7 @@ enum Regex { static let enumCaseDefinition = #/case .* = /# static let imageInitialization = #/(?:UI)?Image\((?:named:)?(?:imageName:)?(?:systemName:)?.*\)/# static let variableToStringConversion = #/"\\(.*)"/# + static let localizedParameter = #/^(?:\.put(?:Number)?\([^)]+\))*/# static let crypto = #/Crypto.*\(/# diff --git a/Session/Closed Groups/NewClosedGroupVC.swift b/Session/Closed Groups/NewClosedGroupVC.swift index 3cec0b768..fd5a15426 100644 --- a/Session/Closed Groups/NewClosedGroupVC.swift +++ b/Session/Closed Groups/NewClosedGroupVC.swift @@ -31,17 +31,33 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate private let dependencies: Dependencies private let contactProfiles: [Profile] + private let hideCloseButton: Bool + private let prefilledName: String? private lazy var data: [ArraySection] = [ ArraySection(model: .contacts, elements: contactProfiles) ] - private var selectedProfiles: [String: Profile] = [:] + private var selectedProfiles: [String: Profile] private var searchText: String = "" // MARK: - Initialization - init(using dependencies: Dependencies) { + init( + hideCloseButton: Bool = false, + prefilledName: String? = nil, + preselectedProfiles: [Profile] = [], + using dependencies: Dependencies + ) { self.dependencies = dependencies - self.contactProfiles = Profile.fetchAllContactProfiles(excludeCurrentUser: true, using: dependencies) + self.hideCloseButton = hideCloseButton + self.prefilledName = prefilledName + self.contactProfiles = Profile + .fetchAllContactProfiles(excludeCurrentUser: true, using: dependencies) + .appending(contentsOf: preselectedProfiles) + .asSet() + .sorted(by: { lhs, rhs -> Bool in lhs.displayName() < rhs.displayName() }) + self.selectedProfiles = preselectedProfiles.reduce(into: [:]) { result, next in + result[next.id] = next + } super.init(nibName: nil, bundle: nil) } @@ -85,6 +101,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate usesDefaultHeight: false, customHeight: NewClosedGroupVC.textFieldHeight ) + result.text = prefilledName result.set(.height, to: NewClosedGroupVC.textFieldHeight) result.themeBorderColor = .borderSeparator result.layer.cornerRadius = 13 @@ -191,11 +208,13 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate let customTitleFontSize = Values.largeFontSize setNavBarTitle("groupCreate".localized(), customFontSize: customTitleFontSize) - let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close)) - closeButton.themeTintColor = .textPrimary - navigationItem.rightBarButtonItem = closeButton - navigationItem.leftBarButtonItem?.accessibilityIdentifier = "Cancel" - navigationItem.leftBarButtonItem?.isAccessibilityElement = true + if !hideCloseButton { + let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close)) + closeButton.themeTintColor = .textPrimary + navigationItem.rightBarButtonItem = closeButton + navigationItem.leftBarButtonItem?.accessibilityIdentifier = "Cancel" + navigationItem.leftBarButtonItem?.isAccessibilityElement = true + } // Set up content setUpViewHierarchy() @@ -378,7 +397,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate let message: String? = (dependencies[feature: .updatedGroups] || selectedProfiles.count <= 20 ? nil : "deleteAfterLegacyGroupsGroupCreation".localized() ) - ModalActivityIndicatorViewController.present(fromViewController: navigationController!, message: message) { [weak self, dependencies] _ in + ModalActivityIndicatorViewController.present(fromViewController: navigationController!, message: message) { [weak self, dependencies] activityIndicatorViewController in let createPublisher: AnyPublisher = { switch dependencies[feature: .updatedGroups] { case true: @@ -422,12 +441,15 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate } }, receiveValue: { thread in + /// When this is triggered via the "Recreate Group" action for Legacy Groups the screen will have been + /// pushed instead of presented and, as a result, we need to dismiss the `activityIndicatorViewController` + /// and want the transition to be animated in order to behave nicely dependencies[singleton: .app].presentConversationCreatingIfNeeded( for: thread.id, variant: thread.variant, action: .none, - dismissing: self?.presentingViewController, - animated: false + dismissing: (self?.presentingViewController ?? activityIndicatorViewController), + animated: (self?.presentingViewController == nil) ) } ) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 54fc0e1cc..70efe23dc 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -35,6 +35,9 @@ extension ConversationVC: } func openSettingsFromTitleView() { + // If we shouldn't be able to access settings then disable the title view shortcuts + guard viewModel.threadData.canAccessSettings(using: viewModel.dependencies) else { return } + switch (titleView.currentLabelType, viewModel.threadData.threadVariant, viewModel.threadData.currentUserIsClosedGroupMember, viewModel.threadData.currentUserIsClosedGroupAdmin) { case (.userCount, .group, _, true), (.userCount, .legacyGroup, _, true): let viewController = SessionTableViewController( @@ -876,6 +879,10 @@ extension ConversationVC: func handleItemLongPressed(_ cellViewModel: MessageViewModel) { // Show the context menu if applicable guard + ( + viewModel.threadData.threadVariant != .legacyGroup || + !viewModel.dependencies[feature: .legacyGroupsDeprecated] + ), // FIXME: Need to update this when an appropriate replacement is added (see https://teng.pub/technical/2021/11/9/uiapplication-key-window-replacement) let keyWindow: UIWindow = UIApplication.shared.keyWindow, let sectionIndex: Int = self.viewModel.interactionData @@ -1463,6 +1470,11 @@ extension ConversationVC: } func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones) { + guard + viewModel.threadData.threadVariant != .legacyGroup || + !viewModel.dependencies[feature: .legacyGroupsDeprecated] + else { return } + react(cellViewModel, with: emoji.rawValue, remove: true) } @@ -2714,6 +2726,90 @@ extension ConversationVC { } } +// MARK: - Legacy Group Actions + +extension ConversationVC { + @objc public func recreateLegacyGroupTapped() { + let threadId: String = self.viewModel.threadData.threadId + let closedGroupName: String? = self.viewModel.threadData.closedGroupName + let confirmationModal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "Recreate Group",//.localized(), + body: .text("Chat history will not be transferred to the new group. You can still view all chat history in your old group."/*.localized()*/), + confirmTitle: "theContinue".localized(), + confirmStyle: .danger, + cancelStyle: .alert_text + ) { [weak self, dependencies = viewModel.dependencies] _ in + let groupMemberProfiles: [WithProfile] = dependencies[singleton: .storage] + .read { db in + try GroupMember + .filter(GroupMember.Columns.groupId == threadId) + .fetchAllWithProfiles(db, using: dependencies) + } + .defaulting(to: []) + let viewController: NewClosedGroupVC = NewClosedGroupVC( + hideCloseButton: true, + prefilledName: closedGroupName, + preselectedProfiles: groupMemberProfiles + .filter { $0.profileId != $0.currentUserSessionId.hexString } + .map { $0.profile ?? Profile(id: $0.profileId, name: "") }, + using: dependencies + ) + + // FIXME: Remove this when we can (it's very fragile) + /// There isn't current a way to animate the change of the `UINavigationBar` background color so instead we + /// insert this `colorAnimationView` on top of the internal `UIBarBackground` and fade it in/out alongside + /// the push/pop transitions + /// + /// **Note:** If we are unable to get the `UIBarBackground` using the below hacks then the navbar will just + /// keep it's existing color and look a bit odd on the destination screen + let colorAnimationView: UIView = UIView( + frame: self?.navigationController?.navigationBar.bounds ?? .zero + ) + colorAnimationView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + colorAnimationView.themeBackgroundColor = .backgroundSecondary + colorAnimationView.alpha = 0 + + if + let navigationBar: UINavigationBar = self?.navigationController?.navigationBar, + let barBackgroundView: UIView = navigationBar.subviews.first(where: { subview -> Bool in + "\(subview)".contains("_UIBarBackground") || ( + subview.subviews.first is UIImageView && + (subview.subviews.first as? UIImageView)?.image == nil + ) + }) + { + barBackgroundView.addSubview(colorAnimationView) + + viewController.onViewWillAppear = { vc in + vc.transitionCoordinator?.animate { _ in + colorAnimationView.alpha = 1 + } + } + viewController.onViewWillDisappear = { vc in + vc.transitionCoordinator?.animate( + alongsideTransition: { _ in + colorAnimationView.alpha = 0 + }, + completion: { _ in + colorAnimationView.removeFromSuperview() + } + ) + } + viewController.onViewDidDisappear = { _ in + // If the screen is dismissed without an animation then 'onViewWillDisappear' + // won't be called so we need to clean up + colorAnimationView.removeFromSuperview() + } + } + self?.navigationController?.pushViewController(viewController, animated: true) + } + ) + + self.navigationController?.present(confirmationModal, animated: true, completion: nil) + } +} + // MARK: - MediaPresentationContextProvider extension ConversationVC: MediaPresentationContextProvider { diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index e0b9f2c2d..5e656bdda 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -130,6 +130,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa var scrollButtonBottomConstraint: NSLayoutConstraint? var scrollButtonMessageRequestsBottomConstraint: NSLayoutConstraint? var messageRequestsViewBotomConstraint: NSLayoutConstraint? + var legacyGroupsFooterViewViewTopConstraint: NSLayoutConstraint? lazy var titleView: ConversationTitleView = { let result: ConversationTitleView = ConversationTitleView(using: viewModel.dependencies) @@ -231,23 +232,16 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa onTap: { [weak self] in self?.removeOutdatedClientBanner() } ) ) + result.isHidden = true return result }() lazy var legacyGroupsBanner: InfoBanner = { - // FIXME: String should be updated in Crowdin to include the {icon} let result: InfoBanner = InfoBanner( info: InfoBanner.Info( - font: .systemFont(ofSize: Values.miniFontSize), - message: "groupLegacyBanner" - .put(key: "date", value: Features.legacyGroupDepricationDate.formattedForBanner) - .localizedFormatted(baseFont: .systemFont(ofSize: Values.miniFontSize)) - .appending(string: " ") // Designs have a space before the icon - .appending( - Lucide.Icon.squareArrowUpRight.attributedString(for: .systemFont(ofSize: Values.miniFontSize)) - ) - .appending(string: " "), // In case it's a RTL font + font: viewModel.legacyGroupsBannerFont, + message: viewModel.legacyGroupsBannerMessage, icon: .none, tintColor: .messageBubble_outgoingText, backgroundColor: .primary, @@ -332,6 +326,46 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa onAccept: { [weak self] in self?.acceptMessageRequest() }, onDecline: { [weak self] in self?.declineMessageRequest() } ) + + private lazy var legacyGroupsRecreateGroupView: UIView = { + let result: UIView = UIView() + result.isHidden = ( + viewModel.threadData.threadVariant != .legacyGroup || + viewModel.threadData.currentUserIsClosedGroupAdmin != true + ) + + return result + }() + + private lazy var legacyGroupsFadeView: GradientView = { + let result: GradientView = GradientView() + result.themeBackgroundGradient = [ + .value(.backgroundPrimary, alpha: 0), // Want this to take up 20% (~20px) + .backgroundPrimary, + .backgroundPrimary, + .backgroundPrimary + ] + result.set(.height, to: 80) + + return result + }() + + private lazy var legacyGroupsInputBackgroundView: UIView = { + let result: UIView = UIView() + result.themeBackgroundColor = .backgroundPrimary + + return result + }() + + private lazy var legacyGroupsFooterButton: SessionButton = { + let result: SessionButton = SessionButton(style: .bordered, size: .medium) + result.translatesAutoresizingMaskIntoConstraints = false + result.setTitle("Recreate Group", for: .normal) + result.addTarget(self, action: #selector(recreateLegacyGroupTapped), for: .touchUpInside) + result.accessibilityIdentifier = "Legacy Groups Recreate Button" + + return result + }() // MARK: - Settings @@ -406,6 +440,11 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa view.addSubview(scrollButton) view.addSubview(stateStackView) view.addSubview(messageRequestFooterView) + view.addSubview(legacyGroupsRecreateGroupView) + + legacyGroupsRecreateGroupView.addSubview(legacyGroupsInputBackgroundView) + legacyGroupsRecreateGroupView.addSubview(legacyGroupsFadeView) + legacyGroupsRecreateGroupView.addSubview(legacyGroupsFooterButton) stateStackView.pin(.top, to: .top, of: view, withInset: 0) stateStackView.pin(.leading, to: .leading, of: view, withInset: 0) @@ -418,6 +457,22 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa self.scrollButtonBottomConstraint = scrollButton.pin(.bottom, to: .bottom, of: view, withInset: -16) self.scrollButtonBottomConstraint?.isActive = false // Note: Need to disable this to avoid a conflict with the other bottom constraint self.scrollButtonMessageRequestsBottomConstraint = scrollButton.pin(.bottom, to: .top, of: messageRequestFooterView, withInset: -4) + + legacyGroupsFooterViewViewTopConstraint = legacyGroupsRecreateGroupView + .pin(.top, to: .bottom, of: view, withInset: -Values.footerGradientHeight(window: UIApplication.shared.keyWindow)) + legacyGroupsRecreateGroupView.pin(.leading, to: .leading, of: view) + legacyGroupsRecreateGroupView.pin(.trailing, to: .trailing, of: view) + legacyGroupsRecreateGroupView.pin(.bottom, to: .bottom, of: view) + legacyGroupsFadeView.pin(.top, to: .top, of: legacyGroupsRecreateGroupView) + legacyGroupsFadeView.pin(.leading, to: .leading, of: legacyGroupsRecreateGroupView) + legacyGroupsFadeView.pin(.trailing, to: .trailing, of: legacyGroupsRecreateGroupView) + legacyGroupsInputBackgroundView.pin(.top, to: .bottom, of: legacyGroupsFadeView) + legacyGroupsInputBackgroundView.pin(.leading, to: .leading, of: legacyGroupsRecreateGroupView) + legacyGroupsInputBackgroundView.pin(.trailing, to: .trailing, of: legacyGroupsRecreateGroupView) + legacyGroupsInputBackgroundView.pin(.bottom, to: .bottom, of: legacyGroupsRecreateGroupView) + legacyGroupsFooterButton.pin(.top, to: .top, of: legacyGroupsFadeView, withInset: 32) + legacyGroupsFooterButton.pin(.leading, to: .leading, of: legacyGroupsFadeView, withInset: 16) + legacyGroupsFooterButton.pin(.trailing, to: .trailing, of: legacyGroupsFadeView, withInset: -16) // Unread count view view.addSubview(unreadCountView) @@ -1268,10 +1323,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa guard let threadData: SessionThreadViewModel = threadData, - ( - threadData.threadRequiresApproval == false && - threadData.threadIsMessageRequest == false - ) + threadData.canAccessSettings(using: viewModel.dependencies) else { // Note: Adding empty buttons because without it the title alignment is busted (Note: The size was // taken from the layout inspector for the back button in Xcode @@ -1352,7 +1404,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa var keyboardEndFrame: CGRect = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return } - // If reduce motion+crossfade transitions is on, in iOS 14 UIKit vends out a keyboard end frame + // If reduce motion+crossfade transitions is on, in iOS 14 UIKit sends out a keyboard end frame // of CGRect zero. This breaks the math below. // // If our keyboard end frame is CGRectZero, build a fake rect that's translated off the bottom edge. @@ -1365,6 +1417,17 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa ) } + // If we explicitly can't write to the thread then the input will be hidden but they keyboard + // still reports that it takes up size, so just report 0 height in that case + if viewModel.threadData.threadCanWrite == false { + keyboardEndFrame = CGRect( + x: UIScreen.main.bounds.minX, + y: UIScreen.main.bounds.maxY, + width: UIScreen.main.bounds.width, + height: 0 + ) + } + // No nothing if there was no change // Note: there is a bug on iOS 15.X for iPhone 6/6s where the converted frame is not accurate. // In iOS 16.1 and later, the keyboard notification object is the screen the keyboard appears on. @@ -1430,6 +1493,7 @@ final class ConversationVC: BaseVC, LibSessionRespondingViewController, Conversa let insetDifference: CGFloat = (contentInsets.bottom - tableView.contentInset.bottom) scrollButtonBottomConstraint?.constant = -(bottomOffset + 12) messageRequestsViewBotomConstraint?.constant = -bottomOffset + legacyGroupsFooterViewViewTopConstraint?.constant = -(legacyGroupsFadeView.bounds.height + bottomOffset + (viewModel.threadData.threadCanWrite == false ? 16 : 0)) tableView.contentInset = contentInsets tableView.scrollIndicatorInsets = contentInsets diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index 85329258f..052ea0af7 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -3,6 +3,7 @@ import Foundation import Combine import UniformTypeIdentifiers +import Lucide import GRDB import DifferenceKit import SessionSnodeKit @@ -63,6 +64,26 @@ public class ConversationViewModel: OWSAudioPlayerDelegate, NavigatableStateHold private var markAsReadPublisher: AnyPublisher? public let dependencies: Dependencies + public let legacyGroupsBannerFont: UIFont = .systemFont(ofSize: Values.miniFontSize) + public lazy var legacyGroupsBannerMessage: NSAttributedString = { + let localizationKey: String + + switch (dependencies[feature: .legacyGroupsDeprecated], threadData.currentUserIsClosedGroupAdmin == true) { + case (false, false): localizationKey = "groupLegacyBanner" + case (false, true): localizationKey = "groupLegacyBanner" + case (true, false): localizationKey = "groupLegacyBanner" + case (true, true): localizationKey = "groupLegacyBanner" + } + + // FIXME: Strings should be updated in Crowdin to include the {icon} + return localizationKey + .put(key: "date", value: Features.legacyGroupDepricationDate.formattedForBanner) + .localizedFormatted(baseFont: legacyGroupsBannerFont) + .appending(string: " ") // Designs have a space before the icon + .appending(Lucide.Icon.squareArrowUpRight.attributedString(for: legacyGroupsBannerFont)) + .appending(string: " ") // In case it's a RTL font + }() + public lazy var blockedBannerMessage: String = { let threadData: SessionThreadViewModel = self.internalThreadData diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 05942e395..025224cc2 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -787,6 +787,9 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi threadViewModel.threadId != threadViewModel.currentUserSessionId && ( threadViewModel.threadVariant != .contact || (try? SessionId(from: section.elements[indexPath.row].threadId))?.prefix == .standard + ) && ( + threadViewModel.threadVariant != .legacyGroup || + !viewModel.dependencies[feature: .legacyGroupsDeprecated] ) else { return nil } @@ -830,10 +833,16 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi let sessionIdPrefix: SessionId.Prefix? = try? SessionId.Prefix(from: threadViewModel.threadId) // Cannot properly sync outgoing blinded message requests so only provide valid options - let shouldHavePinAction: Bool = ( - sessionIdPrefix != .blinded15 && - sessionIdPrefix != .blinded25 - ) + let shouldHavePinAction: Bool = { + switch threadViewModel.threadVariant { + case .legacyGroup: return !viewModel.dependencies[feature: .legacyGroupsDeprecated] + default: + return ( + sessionIdPrefix != .blinded15 && + sessionIdPrefix != .blinded25 + ) + } + }() let shouldHaveMuteAction: Bool = { switch threadViewModel.threadVariant { case .contact: return ( @@ -841,8 +850,11 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi sessionIdPrefix != .blinded15 && sessionIdPrefix != .blinded25 ) + + case .group: return (threadViewModel.currentUserIsClosedGroupMember == true) - case .legacyGroup, .group: return ( + case .legacyGroup: return ( + !viewModel.dependencies[feature: .legacyGroupsDeprecated] && threadViewModel.currentUserIsClosedGroupMember == true ) @@ -850,9 +862,9 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi } }() let destructiveAction: UIContextualAction.SwipeAction = { - switch (threadViewModel.threadVariant, threadViewModel.threadIsNoteToSelf, threadViewModel.currentUserIsClosedGroupMember) { - case (.contact, true, _): return .hide - case (.legacyGroup, _, true), (.group, _, true), (.community, _, _): return .leave + switch (threadViewModel.threadVariant, threadViewModel.threadIsNoteToSelf, threadViewModel.currentUserIsClosedGroupMember, viewModel.dependencies[feature: .legacyGroupsDeprecated]) { + case (.contact, true, _, _): return .hide + case (.legacyGroup, _, true, false), (.group, _, true, _), (.community, _, _, _): return .leave default: return .delete } }() diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift index 23690be81..fef52ae85 100644 --- a/Session/Settings/DeveloperSettingsViewModel.swift +++ b/Session/Settings/DeveloperSettingsViewModel.swift @@ -78,6 +78,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case debugDisappearingMessageDurations case updatedGroups + case legacyGroupsDeprecated case updatedGroupsDisableAutoApprove case updatedGroupsRemoveMessagesOnKick case updatedGroupsAllowHistoricAccessOnInvite @@ -113,6 +114,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .debugDisappearingMessageDurations: return "debugDisappearingMessageDurations" case .updatedGroups: return "updatedGroups" + case .legacyGroupsDeprecated: return "legacyGroupsDeprecated" case .updatedGroupsDisableAutoApprove: return "updatedGroupsDisableAutoApprove" case .updatedGroupsRemoveMessagesOnKick: return "updatedGroupsRemoveMessagesOnKick" case .updatedGroupsAllowHistoricAccessOnInvite: return "updatedGroupsAllowHistoricAccessOnInvite" @@ -151,6 +153,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .debugDisappearingMessageDurations: result.append(.debugDisappearingMessageDurations); fallthrough case .updatedGroups: result.append(.updatedGroups); fallthrough + case .legacyGroupsDeprecated: result.append(.legacyGroupsDeprecated); fallthrough case .updatedGroupsDisableAutoApprove: result.append(.updatedGroupsDisableAutoApprove); fallthrough case .updatedGroupsRemoveMessagesOnKick: result.append(.updatedGroupsRemoveMessagesOnKick); fallthrough case .updatedGroupsAllowHistoricAccessOnInvite: @@ -189,6 +192,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, let updatedDisappearingMessages: Bool let updatedGroups: Bool + let legacyGroupsDeprecated: Bool let updatedGroupsDisableAutoApprove: Bool let updatedGroupsRemoveMessagesOnKick: Bool let updatedGroupsAllowHistoricAccessOnInvite: Bool @@ -220,6 +224,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, updatedDisappearingMessages: dependencies[feature: .updatedDisappearingMessages], updatedGroups: dependencies[feature: .updatedGroups], + legacyGroupsDeprecated: dependencies[feature: .legacyGroupsDeprecated], updatedGroupsDisableAutoApprove: dependencies[feature: .updatedGroupsDisableAutoApprove], updatedGroupsRemoveMessagesOnKick: dependencies[feature: .updatedGroupsRemoveMessagesOnKick], updatedGroupsAllowHistoricAccessOnInvite: dependencies[feature: .updatedGroupsAllowHistoricAccessOnInvite], @@ -475,7 +480,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, id: .updatedGroups, title: "Create Updated Groups", subtitle: """ - Controls whether newly created groups are updated or legacy groups. + Controls whether newly created groups are updated or legacy groups. """, trailingAccessory: .toggle( current.updatedGroups, @@ -488,6 +493,22 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, ) } ), + SessionCell.Info( + id: .legacyGroupsDeprecated, + title: "Legacy Groups Deprecated", + subtitle: """ + Controls whether legacy groups have been deprecated. + + Note: This doesn't affect whether updated or legacy groups are created when creating new groups. + """, + trailingAccessory: .toggle( + current.legacyGroupsDeprecated, + oldValue: previous?.legacyGroupsDeprecated + ), + onTap: { [weak self] in + self?.updateLegacyGroupsDeprecated(to: !current.legacyGroupsDeprecated) + } + ), SessionCell.Info( id: .updatedGroupsDisableAutoApprove, title: "Disable Auto Approve", @@ -731,6 +752,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, case .updatedDisappearingMessages: updateFlag(for: .updatedDisappearingMessages, to: nil) case .updatedGroups: updateFlag(for: .updatedGroups, to: nil) + case .legacyGroupsDeprecated: updateLegacyGroupsDeprecated(to: nil) case .updatedGroupsDisableAutoApprove: updateFlag(for: .updatedGroupsDisableAutoApprove, to: nil) case .updatedGroupsRemoveMessagesOnKick: updateFlag(for: .updatedGroupsRemoveMessagesOnKick, to: nil) case .updatedGroupsAllowHistoricAccessOnInvite: @@ -904,6 +926,17 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, forceRefresh(type: .databaseQuery) } + private func updateLegacyGroupsDeprecated(to updatedFlag: Bool?) { + updateFlag(for: .legacyGroupsDeprecated, to: updatedFlag) + + // Stop and restart the group pollers now that the flag has been updated (legacy groups + // will/won't be started based on the flag) + dependencies.mutate(cache: .groupPollers) { + $0.stopAndRemoveAllPollers() + $0.startAllPollers() + } + } + private func updateForceOffline(current: Bool) { updateFlag(for: .forceOffline, to: !current) diff --git a/Session/Shared/BaseVC.swift b/Session/Shared/BaseVC.swift index c6bff73df..6ca1c9ee8 100644 --- a/Session/Shared/BaseVC.swift +++ b/Session/Shared/BaseVC.swift @@ -4,6 +4,10 @@ import UIKit import SessionUIKit public class BaseVC: UIViewController { + public var onViewWillAppear: ((UIViewController) -> Void)? + public var onViewWillDisappear: ((UIViewController) -> Void)? + public var onViewDidDisappear: ((UIViewController) -> Void)? + public override var preferredStatusBarStyle: UIStatusBarStyle { return ThemeManager.currentTheme.statusBarStyle } @@ -37,6 +41,24 @@ public class BaseVC: UIViewController { setNeedsStatusBarAppearanceUpdate() } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + onViewWillAppear?(self) + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + onViewWillDisappear?(self) + } + + public override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + onViewDidDisappear?(self) + } internal func setNavBarTitle(_ title: String, customFontSize: CGFloat? = nil) { let container = UIView() diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift index d9e5d4354..3b7fb637a 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/GroupPoller.swift @@ -103,6 +103,12 @@ public extension GroupPoller { .fetchSet(db) }? .forEach { [weak self] swarmPublicKey in + // If legacy groups have been deprecated then don't start pollers for them + guard + !dependencies[feature: .legacyGroupsDeprecated] || + (try? SessionId.Prefix(from: swarmPublicKey)) != .standard + else { return } + self?.getOrCreatePoller(for: swarmPublicKey).startIfNeeded() } } diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 9f9ef150f..307597a27 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -300,6 +300,16 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat ) } + public func canAccessSettings(using dependencies: Dependencies) -> Bool { + return ( + threadRequiresApproval == false && + threadIsMessageRequest == false && ( + threadVariant != .legacyGroup || + !dependencies[feature: .legacyGroupsDeprecated] + ) + ) + } + // MARK: - Marking as Read public enum ReadTarget { @@ -407,6 +417,7 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat return (profile?.blocksCommunityMessageRequests != true) case .legacyGroup: + guard !dependencies[feature: .legacyGroupsDeprecated] else { return false } guard threadIsMessageRequest == false else { return true } return ( diff --git a/SessionUtilitiesKit/General/Feature.swift b/SessionUtilitiesKit/General/Feature.swift index c3cd7e59a..8d73725d7 100644 --- a/SessionUtilitiesKit/General/Feature.swift +++ b/SessionUtilitiesKit/General/Feature.swift @@ -44,6 +44,14 @@ public extension FeatureStorage { ) ) + static let legacyGroupsDeprecated: FeatureConfig = Dependencies.create( + identifier: "legacyGroupsDeprecated", + automaticChangeBehaviour: Feature.ChangeBehaviour( + value: true, + condition: .after(timestamp: Features.legacyGroupDepricationDate.timeIntervalSince1970) + ) + ) + static let updatedGroupsDisableAutoApprove: FeatureConfig = Dependencies.create( identifier: "updatedGroupsDisableAutoApprove" )