Merge branch 'charlesmchen/groupsV2migration2'
This commit is contained in:
commit
e04de976bb
@ -235,6 +235,7 @@
|
||||
349EA07C2162AEA800F7B17F /* OWS111UDAttributesMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349EA07B2162AEA700F7B17F /* OWS111UDAttributesMigration.swift */; };
|
||||
349ED990221B0194008045B0 /* Onboarding2FAViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349ED98F221B0194008045B0 /* Onboarding2FAViewController.swift */; };
|
||||
349ED992221EE80D008045B0 /* AppPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349ED991221EE80D008045B0 /* AppPreferences.swift */; };
|
||||
34A17D81253F7237009F8C02 /* ConversationSettingsViewController+LegacyGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A17D80253F7236009F8C02 /* ConversationSettingsViewController+LegacyGroups.swift */; };
|
||||
34A4C61E221613D00042EF2E /* OnboardingVerificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A4C61D221613D00042EF2E /* OnboardingVerificationViewController.swift */; };
|
||||
34A4D56F24E4D342002F8044 /* UnfairLockPerformanceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A4D56E24E4D341002F8044 /* UnfairLockPerformanceTest.swift */; };
|
||||
34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */; };
|
||||
@ -1191,6 +1192,7 @@
|
||||
349EA07B2162AEA700F7B17F /* OWS111UDAttributesMigration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWS111UDAttributesMigration.swift; sourceTree = "<group>"; };
|
||||
349ED98F221B0194008045B0 /* Onboarding2FAViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Onboarding2FAViewController.swift; sourceTree = "<group>"; };
|
||||
349ED991221EE80D008045B0 /* AppPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppPreferences.swift; sourceTree = "<group>"; };
|
||||
34A17D80253F7236009F8C02 /* ConversationSettingsViewController+LegacyGroups.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConversationSettingsViewController+LegacyGroups.swift"; sourceTree = "<group>"; };
|
||||
34A4C61D221613D00042EF2E /* OnboardingVerificationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingVerificationViewController.swift; sourceTree = "<group>"; };
|
||||
34A4D56E24E4D341002F8044 /* UnfairLockPerformanceTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnfairLockPerformanceTest.swift; sourceTree = "<group>"; };
|
||||
34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSImagePickerController.swift; sourceTree = "<group>"; };
|
||||
@ -2176,6 +2178,7 @@
|
||||
34235F3724213550008C74CB /* ConversationSettingsViewController.swift */,
|
||||
34E20D4D2425672A002C011E /* ConversationSettingsViewController+Contents.swift */,
|
||||
34E20D4B24256563002C011E /* ConversationSettingsViewController+Header.swift */,
|
||||
34A17D80253F7236009F8C02 /* ConversationSettingsViewController+LegacyGroups.swift */,
|
||||
340FC89D204DAC8D007AEB0F /* FingerprintViewController.h */,
|
||||
340FC8A2204DAC8D007AEB0F /* FingerprintViewController.m */,
|
||||
340FC8A5204DAC8D007AEB0F /* FingerprintViewScanController.h */,
|
||||
@ -5003,6 +5006,7 @@
|
||||
88A4CC19246CE6810082211F /* DeviceTransferInitialViewController.swift in Sources */,
|
||||
340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */,
|
||||
8835DE01230DBF7E00DC6B66 /* ComposeViewController.swift in Sources */,
|
||||
34A17D81253F7237009F8C02 /* ConversationSettingsViewController+LegacyGroups.swift in Sources */,
|
||||
452037D11EE84975004E4CDF /* DebugUISessionState.m in Sources */,
|
||||
88A4CC12246CE2F50082211F /* QRCodeView.swift in Sources */,
|
||||
D221A09A169C9E5E00537ABF /* main.m in Sources */,
|
||||
|
||||
@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@class TSAttachmentStream;
|
||||
@class TSCall;
|
||||
@class TSErrorMessage;
|
||||
@class TSInfoMessage;
|
||||
@class TSInteraction;
|
||||
@class TSInvalidIdentityKeyErrorMessage;
|
||||
@class TSMessage;
|
||||
@ -59,6 +60,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (void)showFingerprintWithAddress:(SignalServiceAddress *)address;
|
||||
- (void)showConversationSettings;
|
||||
- (void)showConversationSettingsAndShowMemberRequests;
|
||||
- (void)showGroupMigrationLearnMoreActionSheetWithInfoMessage:(TSInfoMessage *)infoMessage;
|
||||
- (void)handleCallTap:(TSCall *)call;
|
||||
- (void)updateSystemContactWithAddress:(SignalServiceAddress *)address
|
||||
withNewNameComponents:(NSPersonNameComponents *)newNameComponents;
|
||||
|
||||
@ -384,7 +384,7 @@ typedef void (^SystemMessageActionBlock)(void);
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSString *)iconNameForGroupUpdate:(GroupUpdateType)type
|
||||
- (nullable NSString *)iconNameForGroupUpdate:(GroupUpdateType)type
|
||||
{
|
||||
NSString *iconName;
|
||||
|
||||
@ -431,6 +431,15 @@ typedef void (^SystemMessageActionBlock)(void);
|
||||
case GroupUpdateTypeDisappearingMessagesState_disabled:
|
||||
iconName = [Theme iconName:ThemeIconTimerDisabled16];
|
||||
break;
|
||||
case GroupUpdateTypeGroupMigrated:
|
||||
iconName = [Theme iconName:ThemeIconMegaphone16];
|
||||
break;
|
||||
case GroupUpdateTypeGroupMigrated_usersInvited:
|
||||
iconName = [Theme iconName:ThemeIconMemberAdded16];
|
||||
break;
|
||||
case GroupUpdateTypeGroupMigrated_usersDropped:
|
||||
iconName = [Theme iconName:ThemeIconGroup16];
|
||||
break;
|
||||
}
|
||||
|
||||
return iconName;
|
||||
@ -447,12 +456,15 @@ typedef void (^SystemMessageActionBlock)(void);
|
||||
|
||||
if (self.viewItem.systemMessageGroupUpdates.count > 0) {
|
||||
for (GroupUpdateCopyItem *update in self.viewItem.systemMessageGroupUpdates) {
|
||||
NSString *iconName = [self iconNameForGroupUpdate:update.type];
|
||||
|
||||
[labelText appendTemplatedImageNamed:iconName
|
||||
font:label.font
|
||||
heightReference:ImageAttachmentHeightReferenceLineHeight];
|
||||
[labelText append:@" " attributes:@{}];
|
||||
NSString *_Nullable iconName = [self iconNameForGroupUpdate:update.type];
|
||||
if (iconName != nil) {
|
||||
[labelText appendTemplatedImageNamed:iconName
|
||||
font:label.font
|
||||
heightReference:ImageAttachmentHeightReferenceLineHeight];
|
||||
[labelText append:@" " attributes:@{}];
|
||||
} else {
|
||||
OWSFailDebug(@"Missing iconName.");
|
||||
}
|
||||
[labelText append:update.text attributes:@{}];
|
||||
|
||||
if (![update isEqual:self.viewItem.systemMessageGroupUpdates.lastObject]) {
|
||||
@ -655,6 +667,21 @@ typedef void (^SystemMessageActionBlock)(void);
|
||||
TSGroupModel *_Nullable oldGroupModel = infoMessage.oldGroupModel;
|
||||
TSGroupModel *_Nullable newGroupModel = infoMessage.newGroupModel;
|
||||
if (oldGroupModel != nil && newGroupModel != nil) {
|
||||
|
||||
for (GroupUpdateCopyItem *update in self.viewItem.systemMessageGroupUpdates) {
|
||||
if (update.type == GroupUpdateTypeGroupMigrated) {
|
||||
return [SystemMessageAction
|
||||
actionWithTitle:CommonStrings.learnMore
|
||||
block:^{
|
||||
[weakSelf.delegate
|
||||
showGroupMigrationLearnMoreActionSheetWithInfoMessage:
|
||||
infoMessage];
|
||||
}
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(
|
||||
self, @"group_migration_learn_more")];
|
||||
}
|
||||
}
|
||||
|
||||
NSMutableSet<SignalServiceAddress *> *newlyRequestingMembers = [NSMutableSet new];
|
||||
[newlyRequestingMembers unionSet:newGroupModel.groupMembership.requestingMembers];
|
||||
[newlyRequestingMembers minusSet:oldGroupModel.groupMembership.requestingMembers];
|
||||
|
||||
@ -1057,7 +1057,7 @@ typedef enum : NSUInteger {
|
||||
}
|
||||
BOOL hasMultiple = noLongerVerifiedAddresses.count > 1;
|
||||
|
||||
ActionSheetController *actionSheet = [[ActionSheetController alloc] initWithTitle:nil message:nil];
|
||||
ActionSheetController *actionSheet = [ActionSheetController new];
|
||||
|
||||
__weak ConversationViewController *weakSelf = self;
|
||||
ActionSheetAction *verifyAction = [[ActionSheetAction alloc]
|
||||
@ -1719,6 +1719,10 @@ typedef enum : NSUInteger {
|
||||
[self.navigationController setViewControllers:viewControllers animated:YES];
|
||||
}
|
||||
|
||||
- (void)showGroupMigrationLearnMoreActionSheetWithInfoMessage:(TSInfoMessage *)infoMessage
|
||||
{
|
||||
}
|
||||
|
||||
- (NSArray<UIViewController *> *)viewControllersUpToSelf
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
@ -89,17 +89,25 @@ class DebugUIGroupsV2: DebugUIPage {
|
||||
}
|
||||
|
||||
if let groupThread = thread as? TSGroupThread {
|
||||
sectionItems.append(OWSTableItem(title: "Try to migrate group (if already migrated).") {
|
||||
sectionItems.append(OWSTableItem(title: "Try to migrate group (already migrated on service).") {
|
||||
Self.migrate(groupThread: groupThread,
|
||||
migrationMode: .alreadyMigratedOnService)
|
||||
})
|
||||
sectionItems.append(OWSTableItem(title: "Try to migrate group (polite).") {
|
||||
sectionItems.append(OWSTableItem(title: "Try to migrate group (polite manual migration).") {
|
||||
Self.migrate(groupThread: groupThread,
|
||||
migrationMode: .migrateToServicePolite)
|
||||
migrationMode: .manualMigrationPolite)
|
||||
})
|
||||
sectionItems.append(OWSTableItem(title: "Try to migrate group (aggressive).") {
|
||||
sectionItems.append(OWSTableItem(title: "Try to migrate group (aggressive manual migration).") {
|
||||
Self.migrate(groupThread: groupThread,
|
||||
migrationMode: .migrateToServiceAggressive)
|
||||
migrationMode: .manualMigrationAggressive)
|
||||
})
|
||||
sectionItems.append(OWSTableItem(title: "Try to migrate group (polite auto migration).") {
|
||||
Self.migrate(groupThread: groupThread,
|
||||
migrationMode: .autoMigrationPolite)
|
||||
})
|
||||
sectionItems.append(OWSTableItem(title: "Try to migrate group (aggressive auto migration).") {
|
||||
Self.migrate(groupThread: groupThread,
|
||||
migrationMode: .autoMigrationAggressive)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -139,15 +139,17 @@ public class BaseGroupMemberViewController: OWSViewController {
|
||||
}
|
||||
|
||||
memberCountWrapper.isHidden = false
|
||||
let format = NSLocalizedString("GROUP_MEMBER_COUNT_FORMAT",
|
||||
comment: "Format string for the group member count indicator. Embeds {{ %1$@ the number of members in the group, %2$@ the maximum number of members in the group. }}.")
|
||||
let format = NSLocalizedString("GROUP_MEMBER_COUNT_WITHOUT_LIMIT_FORMAT",
|
||||
comment: "Format string for the group member count indicator. Embeds {{ the number of members in the group }}.")
|
||||
let memberCount = groupMemberViewDelegate.groupMemberViewGroupMemberCountForDisplay()
|
||||
|
||||
// maxGroupsV2MemberCount will have values like 101, 151 to
|
||||
// leave a slot for the local user. We render the max count - 1.
|
||||
memberCountLabel.text = String(format: format,
|
||||
OWSFormat.formatInt(memberCount),
|
||||
OWSFormat.formatUInt(GroupManager.maxGroupsV2MemberCount - 1))
|
||||
OWSFormat.formatInt(memberCount))
|
||||
if memberCount >= GroupManager.groupsV2MaxGroupSizeRecommended {
|
||||
memberCountLabel.textColor = .ows_accentRed
|
||||
} else {
|
||||
memberCountLabel.textColor = Theme.primaryTextColor
|
||||
}
|
||||
}
|
||||
|
||||
public func removeRecipient(_ recipient: PickedRecipient) {
|
||||
|
||||
@ -93,7 +93,7 @@ extension NewGroupMembersViewController: GroupMemberViewDelegate {
|
||||
}
|
||||
|
||||
func groupMemberViewIsGroupFull() -> Bool {
|
||||
return groupMemberViewGroupMemberCount(withSelf: true) >= GroupManager.maxGroupsV2MemberCount
|
||||
return groupMemberViewGroupMemberCount(withSelf: true) >= GroupManager.groupsV2MaxGroupSizeHardLimit
|
||||
}
|
||||
|
||||
func groupMemberViewIsPreExistingMember(_ recipient: PickedRecipient) -> Bool {
|
||||
|
||||
@ -236,13 +236,7 @@ extension AddGroupMembersViewController: GroupMemberViewDelegate {
|
||||
guard groupThread.isGroupV2Thread else {
|
||||
return false
|
||||
}
|
||||
return groupMemberViewGroupMemberCountForDisplay() >= GroupManager.maxGroupsV2MemberCount
|
||||
}
|
||||
|
||||
func groupMemberViewMaxMemberCount() -> UInt? {
|
||||
return (groupThread.isGroupV2Thread
|
||||
? GroupManager.maxGroupsV2MemberCount
|
||||
: nil)
|
||||
return groupMemberViewGroupMemberCountForDisplay() >= GroupManager.groupsV2MaxGroupSizeHardLimit
|
||||
}
|
||||
|
||||
func groupMemberViewIsPreExistingMember(_ recipient: PickedRecipient) -> Bool {
|
||||
|
||||
@ -86,35 +86,16 @@ extension ConversationSettingsViewController {
|
||||
subviews.append(buildHeaderSubtitleLabel(attributedText: attributedText, font: font))
|
||||
}
|
||||
|
||||
mutating func addLegacyGroupView() -> UIView {
|
||||
mutating func addLegacyGroupView(groupThread: TSGroupThread,
|
||||
viewController: ConversationSettingsViewController) {
|
||||
subviews.append(UIView.spacer(withHeight: 12))
|
||||
|
||||
let bubbleView = UIView()
|
||||
bubbleView.backgroundColor = Theme.secondaryBackgroundColor
|
||||
bubbleView.layer.cornerRadius = 4
|
||||
bubbleView.layoutMargins = UIEdgeInsets(top: 10, leading: 12, bottom: 10, trailing: 12)
|
||||
subviews.append(bubbleView)
|
||||
|
||||
let label = UILabel()
|
||||
let format = NSLocalizedString("GROUPS_LEGACY_GROUP_DESCRIPTION_FORMAT",
|
||||
comment: "Brief explanation of legacy groups. Embeds {{ a \"learn more\" link. }}.")
|
||||
let learnMoreText = NSLocalizedString("GROUPS_LEGACY_GROUP_LEARN_MORE_LINK",
|
||||
comment: "A \"learn more\" link with more information about legacy groups.")
|
||||
let text = String(format: format, learnMoreText)
|
||||
let attributedString = NSMutableAttributedString(string: text)
|
||||
attributedString.setAttributes([
|
||||
.foregroundColor: Theme.accentBlueColor
|
||||
],
|
||||
forSubstring: learnMoreText)
|
||||
label.textColor = Theme.secondaryTextAndIconColor
|
||||
label.font = .ows_dynamicTypeFootnote
|
||||
label.attributedText = attributedString
|
||||
label.numberOfLines = 0
|
||||
label.lineBreakMode = .byWordWrapping
|
||||
bubbleView.addSubview(label)
|
||||
label.autoPinEdgesToSuperviewMargins()
|
||||
|
||||
return bubbleView
|
||||
let migrationInfo = GroupsV2Migration.migrationInfoForManualMigration(groupThread: groupThread)
|
||||
let legacyGroupView = LegacyGroupView(groupThread: groupThread,
|
||||
migrationInfo: migrationInfo,
|
||||
viewController: viewController)
|
||||
legacyGroupView.configure()
|
||||
subviews.append(legacyGroupView)
|
||||
}
|
||||
|
||||
func buildHeaderSubtitleLabel(attributedText: NSAttributedString,
|
||||
@ -187,10 +168,8 @@ extension ConversationSettingsViewController {
|
||||
}
|
||||
|
||||
if groupThread.isGroupV1Thread {
|
||||
let legacyGroupView = builder.addLegacyGroupView()
|
||||
legacyGroupView.isUserInteractionEnabled = true
|
||||
legacyGroupView.addGestureRecognizer(UITapGestureRecognizer(target: self,
|
||||
action: #selector(didTapLegacyGroupView)))
|
||||
builder.addLegacyGroupView(groupThread: groupThread,
|
||||
viewController: self)
|
||||
}
|
||||
|
||||
builder.addLastSubviews()
|
||||
@ -274,112 +253,4 @@ extension ConversationSettingsViewController {
|
||||
return UIView()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Events
|
||||
|
||||
@objc
|
||||
func didTapLegacyGroupView(sender: UIGestureRecognizer) {
|
||||
ExistingLegacyGroupView().present(fromViewController: self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
class ExistingLegacyGroupView: UIView {
|
||||
|
||||
weak var actionSheetController: ActionSheetController?
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func present(fromViewController: UIViewController) {
|
||||
let buildLabel = { () -> UILabel in
|
||||
let label = UILabel()
|
||||
label.textColor = Theme.primaryTextColor
|
||||
label.numberOfLines = 0
|
||||
label.lineBreakMode = .byWordWrapping
|
||||
return label
|
||||
}
|
||||
|
||||
let titleLabel = buildLabel()
|
||||
titleLabel.font = UIFont.ows_dynamicTypeTitle2.ows_semibold
|
||||
titleLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_TITLE",
|
||||
comment: "Title for the 'legacy group' alert view.")
|
||||
|
||||
let section1TitleLabel = buildLabel()
|
||||
section1TitleLabel.font = UIFont.ows_dynamicTypeBody.ows_semibold
|
||||
section1TitleLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_SECTION_1_TITLE",
|
||||
comment: "Title for the first section of the 'legacy group' alert view.")
|
||||
|
||||
let section1BodyLabel = buildLabel()
|
||||
section1BodyLabel.font = .ows_dynamicTypeBody
|
||||
section1BodyLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_SECTION_1_BODY",
|
||||
comment: "Body text for the first section of the 'legacy group' alert view.")
|
||||
|
||||
let section2TitleLabel = buildLabel()
|
||||
section2TitleLabel.font = UIFont.ows_dynamicTypeBody.ows_semibold
|
||||
section2TitleLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_SECTION_2_TITLE",
|
||||
comment: "Title for the second section of the 'legacy group' alert view.")
|
||||
|
||||
let section2BodyLabel = buildLabel()
|
||||
section2BodyLabel.font = .ows_dynamicTypeBody
|
||||
section2BodyLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_SECTION_2_BODY",
|
||||
comment: "Body text for the second section of the 'legacy group' alert view.")
|
||||
|
||||
let section3BodyLabel = buildLabel()
|
||||
section3BodyLabel.font = .ows_dynamicTypeBody
|
||||
section3BodyLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_SECTION_3_BODY",
|
||||
comment: "Body text for the third section of the 'legacy group' alert view.")
|
||||
|
||||
let buttonFont = UIFont.ows_dynamicTypeBodyClamped.ows_semibold
|
||||
let buttonHeight = OWSFlatButton.heightForFont(buttonFont)
|
||||
let okayButton = OWSFlatButton.button(title: CommonStrings.okayButton,
|
||||
font: buttonFont,
|
||||
titleColor: .white,
|
||||
backgroundColor: .ows_accentBlue,
|
||||
target: self,
|
||||
selector: #selector(dismissAlert))
|
||||
okayButton.autoSetDimension(.height, toSize: buttonHeight)
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: [
|
||||
titleLabel,
|
||||
UIView.spacer(withHeight: 28),
|
||||
section1TitleLabel,
|
||||
UIView.spacer(withHeight: 4),
|
||||
section1BodyLabel,
|
||||
UIView.spacer(withHeight: 21),
|
||||
section2TitleLabel,
|
||||
UIView.spacer(withHeight: 4),
|
||||
section2BodyLabel,
|
||||
UIView.spacer(withHeight: 24),
|
||||
section3BodyLabel,
|
||||
UIView.spacer(withHeight: 28),
|
||||
okayButton
|
||||
])
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .fill
|
||||
stackView.layoutMargins = UIEdgeInsets(top: 48, leading: 20, bottom: 38, trailing: 24)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
stackView.addBackgroundView(withBackgroundColor: Theme.backgroundColor)
|
||||
|
||||
layoutMargins = .zero
|
||||
addSubview(stackView)
|
||||
stackView.autoPinEdgesToSuperviewMargins()
|
||||
|
||||
let actionSheetController = ActionSheetController()
|
||||
actionSheetController.customHeader = self
|
||||
actionSheetController.isCancelable = true
|
||||
fromViewController.presentActionSheet(actionSheetController)
|
||||
self.actionSheetController = actionSheetController
|
||||
}
|
||||
|
||||
@objc
|
||||
func dismissAlert() {
|
||||
actionSheetController?.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,635 @@
|
||||
//
|
||||
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import PromiseKit
|
||||
|
||||
class LegacyGroupView: UIView {
|
||||
|
||||
private let groupThread: TSGroupThread
|
||||
private let migrationInfo: GroupsV2MigrationInfo
|
||||
private weak var viewController: UIViewController?
|
||||
|
||||
required init(groupThread: TSGroupThread,
|
||||
migrationInfo: GroupsV2MigrationInfo,
|
||||
viewController: UIViewController) {
|
||||
self.groupThread = groupThread
|
||||
self.migrationInfo = migrationInfo
|
||||
self.viewController = viewController
|
||||
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
@available(*, unavailable, message: "use other init() instead.")
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
private let label = UILabel()
|
||||
|
||||
func configureLabel(format: String, highlightedSubstring: String) {
|
||||
let text = String(format: format, highlightedSubstring)
|
||||
let attributedString = NSMutableAttributedString(string: text)
|
||||
attributedString.setAttributes([
|
||||
.foregroundColor: Theme.accentBlueColor
|
||||
],
|
||||
forSubstring: highlightedSubstring)
|
||||
label.attributedText = attributedString
|
||||
}
|
||||
|
||||
func configureDefaultLabelContents() {
|
||||
let format = NSLocalizedString("GROUPS_LEGACY_GROUP_DESCRIPTION_FORMAT",
|
||||
comment: "Brief explanation of legacy groups. Embeds {{ a \"learn more\" link. }}.")
|
||||
let learnMoreText = NSLocalizedString("GROUPS_LEGACY_GROUP_LEARN_MORE_LINK",
|
||||
comment: "A \"learn more\" link with more information about legacy groups.")
|
||||
configureLabel(format: format, highlightedSubstring: learnMoreText)
|
||||
|
||||
isUserInteractionEnabled = true
|
||||
addGestureRecognizer(UITapGestureRecognizer(target: self,
|
||||
action: #selector(didTapLearnMore)))
|
||||
}
|
||||
|
||||
func configureCantUpgradeDueToMembersContents() {
|
||||
let format = NSLocalizedString("GROUPS_LEGACY_GROUP_DESCRIPTION_MEMBERS_CANT_BE_MIGRATED_FORMAT",
|
||||
comment: "Indicates that a legacy group can't be upgraded because some members can't be migrated. Embeds {{ an \"learn more\" link. }}.")
|
||||
let learnMoreText = NSLocalizedString("GROUPS_LEGACY_GROUP_LEARN_MORE_LINK",
|
||||
comment: "A \"learn more\" link with more information about legacy groups.")
|
||||
configureLabel(format: format, highlightedSubstring: learnMoreText)
|
||||
|
||||
isUserInteractionEnabled = true
|
||||
addGestureRecognizer(UITapGestureRecognizer(target: self,
|
||||
action: #selector(didTapCantUpgradeDueToMemberState)))
|
||||
}
|
||||
|
||||
public func configure() {
|
||||
backgroundColor = Theme.secondaryBackgroundColor
|
||||
layer.cornerRadius = 4
|
||||
layoutMargins = UIEdgeInsets(top: 10, leading: 12, bottom: 10, trailing: 12)
|
||||
|
||||
label.textColor = Theme.secondaryTextAndIconColor
|
||||
label.font = .ows_dynamicTypeFootnote
|
||||
label.numberOfLines = 0
|
||||
label.lineBreakMode = .byWordWrapping
|
||||
addSubview(label)
|
||||
label.autoPinEdgesToSuperviewMargins()
|
||||
|
||||
switch migrationInfo.state {
|
||||
case .canBeMigrated:
|
||||
let format = NSLocalizedString("GROUPS_LEGACY_GROUP_DESCRIPTION_WITH_UPGRADE_OFFER_FORMAT",
|
||||
comment: "Explanation of legacy groups. Embeds {{ an \"upgrade\" link. }}.")
|
||||
let upgradeText = NSLocalizedString("GROUPS_LEGACY_GROUP_UPGRADE_LINK",
|
||||
comment: "An \"upgrade\" link for upgrading legacy groups to new groups.")
|
||||
configureLabel(format: format, highlightedSubstring: upgradeText)
|
||||
|
||||
isUserInteractionEnabled = true
|
||||
addGestureRecognizer(UITapGestureRecognizer(target: self,
|
||||
action: #selector(didTapUpgrade)))
|
||||
case .cantBeMigrated_FeatureNotEnabled:
|
||||
configureDefaultLabelContents()
|
||||
case .cantBeMigrated_NotAV1Group:
|
||||
owsFailDebug("Unexpected group.")
|
||||
configureDefaultLabelContents()
|
||||
case .cantBeMigrated_NotRegistered:
|
||||
owsFailDebug("Not registered.")
|
||||
configureDefaultLabelContents()
|
||||
case .cantBeMigrated_LocalUserIsNotAMember:
|
||||
Logger.verbose("Local user is not a member.")
|
||||
configureDefaultLabelContents()
|
||||
case .cantBeMigrated_NotInProfileWhitelist:
|
||||
// TODO: Should we special-case this?
|
||||
Logger.verbose("Local user has not accepted message request.")
|
||||
configureDefaultLabelContents()
|
||||
case .cantBeMigrated_TooManyMembers:
|
||||
let format = NSLocalizedString("GROUPS_LEGACY_GROUP_DESCRIPTION_TOO_MANY_MEMBERS_FORMAT",
|
||||
comment: "Indicates that a legacy group can't be upgraded because it has too many members. Embeds {{ an \"learn more\" link. }}.")
|
||||
let learnMoreText = NSLocalizedString("GROUPS_LEGACY_GROUP_LEARN_MORE_LINK",
|
||||
comment: "A \"learn more\" link with more information about legacy groups.")
|
||||
configureLabel(format: format, highlightedSubstring: learnMoreText)
|
||||
|
||||
isUserInteractionEnabled = true
|
||||
addGestureRecognizer(UITapGestureRecognizer(target: self,
|
||||
action: #selector(didTapTooManyMembers)))
|
||||
case .cantBeMigrated_MembersWithoutUuids,
|
||||
.cantBeMigrated_MembersWithoutCapabilities:
|
||||
configureCantUpgradeDueToMembersContents()
|
||||
case .cantBeMigrated_MembersWithoutProfileKey:
|
||||
owsFailDebug("Manual migrations should ignore missing profile keys.")
|
||||
configureCantUpgradeDueToMembersContents()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Events
|
||||
|
||||
@objc
|
||||
public func didTapLearnMore() {
|
||||
guard let viewController = viewController else {
|
||||
owsFailDebug("Missing viewController.")
|
||||
return
|
||||
}
|
||||
LegacyGroupViewLearnMoreView().present(fromViewController: viewController)
|
||||
}
|
||||
|
||||
@objc
|
||||
public func didTapUpgrade() {
|
||||
showMigrationAlert(mode: .upgradeGroup)
|
||||
}
|
||||
|
||||
@objc
|
||||
public func didTapTooManyMembers() {
|
||||
showMigrationAlert(mode: .tooManyMembers)
|
||||
}
|
||||
|
||||
@objc
|
||||
public func didTapCantUpgradeDueToMemberState() {
|
||||
showMigrationAlert(mode: .someMembersCantMigrate)
|
||||
}
|
||||
|
||||
private func showMigrationAlert(mode: LegacyGroupMigrationView.Mode) {
|
||||
guard let viewController = viewController else {
|
||||
owsFailDebug("Missing viewController.")
|
||||
return
|
||||
}
|
||||
let view = LegacyGroupMigrationView(groupThread: groupThread,
|
||||
mode: mode,
|
||||
migrationInfo: migrationInfo)
|
||||
view.present(fromViewController: viewController)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private class LegacyGroupViewLearnMoreView: UIView {
|
||||
|
||||
weak var actionSheetController: ActionSheetController?
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func present(fromViewController: UIViewController) {
|
||||
let buildLabel = { () -> UILabel in
|
||||
let label = UILabel()
|
||||
label.textColor = Theme.primaryTextColor
|
||||
label.numberOfLines = 0
|
||||
label.lineBreakMode = .byWordWrapping
|
||||
return label
|
||||
}
|
||||
|
||||
let titleLabel = buildLabel()
|
||||
titleLabel.font = UIFont.ows_dynamicTypeTitle2.ows_semibold
|
||||
titleLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_TITLE",
|
||||
comment: "Title for the 'legacy group' alert view.")
|
||||
|
||||
let section1TitleLabel = buildLabel()
|
||||
section1TitleLabel.font = UIFont.ows_dynamicTypeBody.ows_semibold
|
||||
section1TitleLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_SECTION_1_TITLE",
|
||||
comment: "Title for the first section of the 'legacy group' alert view.")
|
||||
|
||||
let section1BodyLabel = buildLabel()
|
||||
section1BodyLabel.font = .ows_dynamicTypeBody
|
||||
section1BodyLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_SECTION_1_BODY",
|
||||
comment: "Body text for the first section of the 'legacy group' alert view.")
|
||||
|
||||
let section2TitleLabel = buildLabel()
|
||||
section2TitleLabel.font = UIFont.ows_dynamicTypeBody.ows_semibold
|
||||
section2TitleLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_SECTION_2_TITLE",
|
||||
comment: "Title for the second section of the 'legacy group' alert view.")
|
||||
|
||||
let section2BodyLabel = buildLabel()
|
||||
section2BodyLabel.font = .ows_dynamicTypeBody
|
||||
section2BodyLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_SECTION_2_BODY",
|
||||
comment: "Body text for the second section of the 'legacy group' alert view.")
|
||||
|
||||
let section3BodyLabel = buildLabel()
|
||||
section3BodyLabel.font = .ows_dynamicTypeBody
|
||||
section3BodyLabel.text = NSLocalizedString("GROUPS_LEGACY_GROUP_ALERT_SECTION_3_BODY",
|
||||
comment: "Body text for the third section of the 'legacy group' alert view.")
|
||||
|
||||
let buttonFont = UIFont.ows_dynamicTypeBodyClamped.ows_semibold
|
||||
let buttonHeight = OWSFlatButton.heightForFont(buttonFont)
|
||||
let okayButton = OWSFlatButton.button(title: CommonStrings.okayButton,
|
||||
font: buttonFont,
|
||||
titleColor: .white,
|
||||
backgroundColor: .ows_accentBlue,
|
||||
target: self,
|
||||
selector: #selector(dismissAlert))
|
||||
okayButton.autoSetDimension(.height, toSize: buttonHeight)
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: [
|
||||
titleLabel,
|
||||
UIView.spacer(withHeight: 28),
|
||||
section1TitleLabel,
|
||||
UIView.spacer(withHeight: 4),
|
||||
section1BodyLabel,
|
||||
UIView.spacer(withHeight: 21),
|
||||
section2TitleLabel,
|
||||
UIView.spacer(withHeight: 4),
|
||||
section2BodyLabel,
|
||||
UIView.spacer(withHeight: 24),
|
||||
section3BodyLabel,
|
||||
UIView.spacer(withHeight: 28),
|
||||
okayButton
|
||||
])
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .fill
|
||||
stackView.layoutMargins = UIEdgeInsets(top: 48, leading: 20, bottom: 38, trailing: 24)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
stackView.addBackgroundView(withBackgroundColor: Theme.backgroundColor)
|
||||
|
||||
layoutMargins = .zero
|
||||
addSubview(stackView)
|
||||
stackView.autoPinEdgesToSuperviewMargins()
|
||||
|
||||
let actionSheetController = ActionSheetController()
|
||||
actionSheetController.customHeader = self
|
||||
actionSheetController.isCancelable = true
|
||||
fromViewController.presentActionSheet(actionSheetController)
|
||||
self.actionSheetController = actionSheetController
|
||||
}
|
||||
|
||||
// MARK: - Events
|
||||
|
||||
@objc
|
||||
func dismissAlert() {
|
||||
actionSheetController?.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private class LegacyGroupMigrationView: UIView {
|
||||
|
||||
enum Mode {
|
||||
case upgradeGroup
|
||||
case tooManyMembers
|
||||
case someMembersCantMigrate
|
||||
}
|
||||
|
||||
private let groupThread: TSGroupThread
|
||||
private let mode: Mode
|
||||
private let migrationInfo: GroupsV2MigrationInfo
|
||||
|
||||
weak var actionSheetController: ActionSheetController?
|
||||
|
||||
required init(groupThread: TSGroupThread,
|
||||
mode: Mode,
|
||||
migrationInfo: GroupsV2MigrationInfo) {
|
||||
self.groupThread = groupThread
|
||||
self.mode = mode
|
||||
self.migrationInfo = migrationInfo
|
||||
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func present(fromViewController: UIViewController) {
|
||||
let subviews = buildContents()
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: subviews)
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .fill
|
||||
stackView.layoutMargins = UIEdgeInsets(top: 48, leading: 20, bottom: 38, trailing: 24)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
stackView.addBackgroundView(withBackgroundColor: Theme.backgroundColor)
|
||||
|
||||
layoutMargins = .zero
|
||||
addSubview(stackView)
|
||||
stackView.autoPinEdgesToSuperviewMargins()
|
||||
|
||||
let actionSheetController = ActionSheetController(isFullWidth: true)
|
||||
actionSheetController.customHeader = self
|
||||
actionSheetController.isCancelable = true
|
||||
fromViewController.presentActionSheet(actionSheetController)
|
||||
self.actionSheetController = actionSheetController
|
||||
}
|
||||
|
||||
private struct Builder {
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private static var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
var subviews = [UIView]()
|
||||
|
||||
func buildLabel() -> UILabel {
|
||||
let label = UILabel()
|
||||
label.textColor = Theme.primaryTextColor
|
||||
label.numberOfLines = 0
|
||||
label.lineBreakMode = .byWordWrapping
|
||||
return label
|
||||
}
|
||||
|
||||
func buildTitleLabel(text: String) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.textColor = Theme.primaryTextColor
|
||||
label.font = UIFont.ows_dynamicTypeTitle2.ows_semibold
|
||||
label.numberOfLines = 0
|
||||
label.lineBreakMode = .byWordWrapping
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}
|
||||
|
||||
mutating func addTitleLabel(text: String) {
|
||||
subviews.append(buildTitleLabel(text: text))
|
||||
}
|
||||
|
||||
mutating func addVerticalSpacer(height: CGFloat) {
|
||||
subviews.append(UIView.spacer(withHeight: height))
|
||||
}
|
||||
|
||||
mutating func addRow(subview: UIView, hasBulletPoint: Bool) {
|
||||
|
||||
let bulletSize = CGSize(width: 5, height: 11)
|
||||
let bulletWrapper = UIView.container()
|
||||
bulletWrapper.autoSetDimension(.width, toSize: bulletSize.width)
|
||||
|
||||
if hasBulletPoint {
|
||||
let bullet = UIView()
|
||||
bullet.autoSetDimensions(to: bulletSize)
|
||||
// TODO: Dark theme value?
|
||||
bullet.backgroundColor = UIColor(rgbHex: 0xdedede)
|
||||
bulletWrapper.addSubview(bullet)
|
||||
bullet.autoPinEdge(toSuperviewEdge: .top, withInset: 4)
|
||||
bullet.autoPinEdge(toSuperviewEdge: .leading)
|
||||
bullet.autoPinEdge(toSuperviewEdge: .trailing)
|
||||
}
|
||||
|
||||
bulletWrapper.setContentHuggingHorizontalHigh()
|
||||
subview.setContentHuggingHorizontalLow()
|
||||
subview.setCompressionResistanceVerticalHigh()
|
||||
|
||||
let row = UIStackView(arrangedSubviews: [bulletWrapper, subview])
|
||||
row.axis = .horizontal
|
||||
row.alignment = .top
|
||||
row.spacing = 20
|
||||
row.setCompressionResistanceVerticalHigh()
|
||||
subviews.append(row)
|
||||
}
|
||||
|
||||
mutating func addBodyLabel(_ text: String) {
|
||||
let label = buildLabel()
|
||||
label.font = .ows_dynamicTypeBody
|
||||
label.text = text
|
||||
addRow(subview: label, hasBulletPoint: true)
|
||||
}
|
||||
|
||||
mutating func addMemberRow(address: SignalServiceAddress,
|
||||
transaction: SDSAnyReadTransaction) {
|
||||
|
||||
let avatarSize: UInt = 28
|
||||
let conversationColorName = TSContactThread.conversationColorName(forContactAddress: address,
|
||||
transaction: transaction)
|
||||
let avatarBuilder = OWSContactAvatarBuilder(address: address,
|
||||
colorName: conversationColorName,
|
||||
diameter: avatarSize,
|
||||
transaction: transaction)
|
||||
let avatar = avatarBuilder.build(with: transaction)
|
||||
|
||||
let avatarView = AvatarImageView()
|
||||
avatarView.image = avatar
|
||||
avatarView.autoSetDimensions(to: CGSize(square: CGFloat(avatarSize)))
|
||||
avatarView.setContentHuggingHorizontalHigh()
|
||||
|
||||
let label = buildLabel()
|
||||
label.font = .ows_dynamicTypeBody
|
||||
label.text = Self.contactsManager.displayName(for: address, transaction: transaction)
|
||||
label.setContentHuggingHorizontalLow()
|
||||
|
||||
let row = UIStackView(arrangedSubviews: [avatarView, label])
|
||||
row.axis = .horizontal
|
||||
row.alignment = .center
|
||||
row.spacing = 6
|
||||
row.setContentHuggingHorizontalLow()
|
||||
|
||||
addRow(subview: row, hasBulletPoint: false)
|
||||
}
|
||||
|
||||
mutating func addBottomButton(title: String,
|
||||
titleColor: UIColor,
|
||||
backgroundColor: UIColor,
|
||||
target: Any,
|
||||
selector: Selector) {
|
||||
let buttonFont = UIFont.ows_dynamicTypeBodyClamped.ows_semibold
|
||||
let buttonHeight = OWSFlatButton.heightForFont(buttonFont)
|
||||
let upgradeButton = OWSFlatButton.button(title: title,
|
||||
font: buttonFont,
|
||||
titleColor: titleColor,
|
||||
backgroundColor: backgroundColor,
|
||||
target: target,
|
||||
selector: selector)
|
||||
upgradeButton.autoSetDimension(.height, toSize: buttonHeight)
|
||||
subviews.append(upgradeButton)
|
||||
}
|
||||
|
||||
mutating func addOkayButton(target: Any, selector: Selector) {
|
||||
addBottomButton(title: CommonStrings.okayButton,
|
||||
titleColor: .white,
|
||||
backgroundColor: .ows_accentBlue,
|
||||
target: target,
|
||||
selector: selector)
|
||||
}
|
||||
}
|
||||
|
||||
private func buildContents() -> [UIView] {
|
||||
switch mode {
|
||||
case .upgradeGroup:
|
||||
return buildUpgradeGroupContents()
|
||||
case .tooManyMembers:
|
||||
return buildTooManyMembersContents()
|
||||
case .someMembersCantMigrate:
|
||||
return buildSomeMembersCantMigrateContents()
|
||||
}
|
||||
}
|
||||
|
||||
private func buildUpgradeGroupContents() -> [UIView] {
|
||||
var builder = Builder()
|
||||
|
||||
builder.addTitleLabel(text: NSLocalizedString("GROUPS_LEGACY_GROUP_UPGRADE_ALERT_TITLE",
|
||||
comment: "Title for the 'upgrade legacy group' alert view."))
|
||||
builder.addVerticalSpacer(height: 28)
|
||||
|
||||
builder.addBodyLabel(NSLocalizedString("GROUPS_LEGACY_GROUP_NEW_GROUP_DESCRIPTION",
|
||||
comment: "Explanation of new groups in the 'legacy group' alert views."))
|
||||
builder.addVerticalSpacer(height: 20)
|
||||
builder.addBodyLabel(NSLocalizedString("GROUPS_LEGACY_GROUP_UPGRADE_ALERT_SECTION_2_BODY",
|
||||
comment: "Body text for the second section of the 'upgrade legacy group' alert view."))
|
||||
|
||||
let migrationInfo = self.migrationInfo
|
||||
databaseStorage.read { transaction in
|
||||
// TODO: We need to break these out into separate sections.
|
||||
// TODO: Scroll view?
|
||||
// let members = (migrationInfo.membersWithoutUuids +
|
||||
// migrationInfo.membersWithoutCapabilities +
|
||||
// migrationInfo.membersWithoutProfileKeys)
|
||||
var members = (migrationInfo.membersWithoutUuids +
|
||||
migrationInfo.membersWithoutCapabilities +
|
||||
migrationInfo.membersWithoutProfileKeys)
|
||||
members = members + members
|
||||
members = members + members
|
||||
members = members + members
|
||||
members = members + members
|
||||
if !members.isEmpty {
|
||||
builder.addVerticalSpacer(height: 20)
|
||||
builder.addBodyLabel(NSLocalizedString("GROUPS_LEGACY_GROUP_UPGRADE_ALERT_SECTION_INVITED_MEMBERS",
|
||||
comment: "Body text for the 'invites members' section of the 'upgrade legacy group' alert view."))
|
||||
for address in members {
|
||||
builder.addVerticalSpacer(height: 16)
|
||||
builder.addMemberRow(address: address, transaction: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.addVerticalSpacer(height: 40)
|
||||
|
||||
builder.addBottomButton(title: NSLocalizedString("GROUPS_LEGACY_GROUP_UPGRADE_ALERT_UPGRADE_BUTTON",
|
||||
comment: "Label for the 'upgrade this group' button in the 'upgrade legacy group' alert view."),
|
||||
titleColor: .white,
|
||||
backgroundColor: .ows_accentBlue,
|
||||
target: self,
|
||||
selector: #selector(upgradeGroup))
|
||||
builder.addVerticalSpacer(height: 5)
|
||||
builder.addBottomButton(title: CommonStrings.cancelButton,
|
||||
titleColor: .ows_accentBlue,
|
||||
backgroundColor: .white,
|
||||
target: self,
|
||||
selector: #selector(dismissAlert))
|
||||
|
||||
return builder.subviews
|
||||
}
|
||||
|
||||
private func buildTooManyMembersContents() -> [UIView] {
|
||||
var builder = Builder()
|
||||
|
||||
builder.addTitleLabel(text: NSLocalizedString("GROUPS_LEGACY_GROUP_CANT_UPGRADE_ALERT_TITLE",
|
||||
comment: "Title for the 'can't upgrade legacy group' alert view."))
|
||||
builder.addVerticalSpacer(height: 28)
|
||||
|
||||
builder.addBodyLabel(NSLocalizedString("GROUPS_LEGACY_GROUP_NEW_GROUP_DESCRIPTION",
|
||||
comment: "Explanation of new groups in the 'legacy group' alert views."))
|
||||
builder.addVerticalSpacer(height: 20)
|
||||
let descriptionFormat = NSLocalizedString("GROUPS_LEGACY_GROUP_CANT_UPGRADE_ALERT_TOO_MANY_MEMBERS_FORMAT",
|
||||
comment: "Text indicating that a legacy group can't be upgraded because it has too many members. Embeds {{ The maximum number of members allowed in a group. }}.")
|
||||
let maxMemberCount = OWSFormat.formatUInt(RemoteConfig.groupsV2MaxGroupSizeHardLimit - 1)
|
||||
let description = String(format: descriptionFormat, maxMemberCount)
|
||||
builder.addBodyLabel(description)
|
||||
|
||||
builder.addVerticalSpacer(height: 40)
|
||||
|
||||
builder.addOkayButton(target: self, selector: #selector(dismissAlert))
|
||||
|
||||
return builder.subviews
|
||||
}
|
||||
|
||||
private func buildSomeMembersCantMigrateContents() -> [UIView] {
|
||||
var builder = Builder()
|
||||
|
||||
builder.addTitleLabel(text: NSLocalizedString("GROUPS_LEGACY_GROUP_NEW_GROUPS_ALERT_TITLE",
|
||||
comment: "Title for the 'new groups' alert view."))
|
||||
builder.addVerticalSpacer(height: 28)
|
||||
|
||||
builder.addBodyLabel(NSLocalizedString("GROUPS_LEGACY_GROUP_NEW_GROUP_DESCRIPTION",
|
||||
comment: "Explanation of new groups in the 'legacy group' alert views."))
|
||||
builder.addVerticalSpacer(height: 20)
|
||||
builder.addBodyLabel(NSLocalizedString("GROUPS_LEGACY_GROUP_CANT_UPGRADE_YET_1",
|
||||
comment: "Explanation group migration for groups that can't yet be migrated in the 'legacy group' alert views."))
|
||||
builder.addVerticalSpacer(height: 20)
|
||||
builder.addBodyLabel(NSLocalizedString("GROUPS_LEGACY_GROUP_CANT_UPGRADE_YET_2",
|
||||
comment: "Explanation group migration for groups that can't yet be migrated in the 'legacy group' alert views."))
|
||||
|
||||
builder.addVerticalSpacer(height: 40)
|
||||
|
||||
builder.addOkayButton(target: self, selector: #selector(dismissAlert))
|
||||
|
||||
return builder.subviews
|
||||
}
|
||||
|
||||
// MARK: - Events
|
||||
|
||||
@objc
|
||||
func dismissAlert() {
|
||||
actionSheetController?.dismiss(animated: true)
|
||||
}
|
||||
|
||||
@objc
|
||||
func upgradeGroup() {
|
||||
guard let actionSheetController = actionSheetController else {
|
||||
owsFailDebug("Missing actionSheetController.")
|
||||
return
|
||||
}
|
||||
ModalActivityIndicatorViewController.present(fromViewController: actionSheetController,
|
||||
canCancel: false) { modalActivityIndicator in
|
||||
firstly {
|
||||
self.upgradePromise()
|
||||
}.done { (_) in
|
||||
modalActivityIndicator.dismiss {
|
||||
self.dismissAndShowUpgradeSuccessToast()
|
||||
}
|
||||
}.catch { error in
|
||||
owsFailDebug("Error: \(error)")
|
||||
|
||||
modalActivityIndicator.dismiss {
|
||||
self.showUpgradeFailedAlert(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func upgradePromise() -> Promise<Void> {
|
||||
GroupsV2Migration.tryManualMigration(groupThread: groupThread).asVoid()
|
||||
}
|
||||
|
||||
private func dismissAndShowUpgradeSuccessToast() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let actionSheetController = actionSheetController else {
|
||||
owsFailDebug("Missing actionSheetController.")
|
||||
return
|
||||
}
|
||||
|
||||
actionSheetController.dismiss(animated: true) {
|
||||
self.showUpgradeSuccessToast()
|
||||
}
|
||||
}
|
||||
|
||||
private func showUpgradeSuccessToast() {
|
||||
guard let viewController = UIApplication.shared.frontmostViewController else {
|
||||
owsFailDebug("Missing frontmostViewController.")
|
||||
return
|
||||
}
|
||||
|
||||
let text = NSLocalizedString("GROUPS_LEGACY_GROUP_UPGRADE_ALERT_UPGRADE_SUCCEEDED",
|
||||
comment: "Message indicating the group update succeeded.")
|
||||
let toastController = ToastController(text: text)
|
||||
let toastInset = viewController.bottomLayoutGuide.length + 8
|
||||
toastController.presentToastView(fromBottomOfView: viewController.view, inset: toastInset)
|
||||
}
|
||||
|
||||
private func showUpgradeFailedAlert(error: Error) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let actionSheetController = actionSheetController else {
|
||||
owsFailDebug("Missing actionSheetController.")
|
||||
return
|
||||
}
|
||||
|
||||
let title: String
|
||||
// TODO: We need final copy.
|
||||
title = NSLocalizedString("GROUPS_LEGACY_GROUP_UPGRADE_ALERT_UPGRADE_FAILED_ERROR",
|
||||
comment: "Error indicating the group update failed.")
|
||||
OWSActionSheets.showActionSheet(title: title, fromViewController: actionSheetController)
|
||||
}
|
||||
}
|
||||
@ -53,7 +53,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return;
|
||||
}
|
||||
|
||||
ActionSheetController *actionSheet = [[ActionSheetController alloc] initWithTitle:nil message:nil];
|
||||
ActionSheetController *actionSheet = [ActionSheetController new];
|
||||
|
||||
[actionSheet
|
||||
addAction:[[ActionSheetAction alloc]
|
||||
|
||||
@ -1810,9 +1810,6 @@
|
||||
/* Label indicating that a group member is an admin. */
|
||||
"GROUP_MEMBER_ADMIN_INDICATOR" = "Admin";
|
||||
|
||||
/* Format string for the group member count indicator. Embeds {{ %1$@ the number of members in the group, %2$@ the maximum number of members in the group. }}. */
|
||||
"GROUP_MEMBER_COUNT_FORMAT" = "%1$@/%2$@";
|
||||
|
||||
/* The 'group member count' indicator when there are no members in the group. */
|
||||
"GROUP_MEMBER_COUNT_LABEL_0" = "No members";
|
||||
|
||||
@ -1822,6 +1819,9 @@
|
||||
/* Format for the 'group member count' indicator. Embeds {the number of group members}. */
|
||||
"GROUP_MEMBER_COUNT_LABEL_FORMAT" = "%@ members";
|
||||
|
||||
/* Format string for the group member count indicator. Embeds {{ the number of members in the group }}. */
|
||||
"GROUP_MEMBER_COUNT_WITHOUT_LIMIT_FORMAT" = "%@ Members";
|
||||
|
||||
/* Label indicating the local user. */
|
||||
"GROUP_MEMBER_LOCAL_USER" = "You";
|
||||
|
||||
@ -2014,6 +2014,21 @@
|
||||
/* Message indicating that the group's name was changed. Embeds {{new group name}}. */
|
||||
"GROUP_UPDATED_NAME_UPDATED_FORMAT" = "Group name is now “%@”.";
|
||||
|
||||
/* Message indicating that the group was migrated. */
|
||||
"GROUP_WAS_MIGRATED" = "This group was upgraded to a New Group.";
|
||||
|
||||
/* Message indicating that 1 user was dropped while migrating the group. */
|
||||
"GROUP_WAS_MIGRATED_USERS_DROPPED_1" = "A member couldn’t be added to the New Group and has been removed from the group.";
|
||||
|
||||
/* Message indicating that N users were dropped while migrating the group. Embeds {{ the number of dropped users }}. */
|
||||
"GROUP_WAS_MIGRATED_USERS_DROPPED_N_FORMAT" = "%@ members couldn’t be added to the New Group and have been removed from the group.";
|
||||
|
||||
/* Message indicating that 1 user was invited while migrating the group. */
|
||||
"GROUP_WAS_MIGRATED_USERS_INVITED_1" = "A member couldn’t be added to the New Group and has been invited to join";
|
||||
|
||||
/* Message indicating that N users were invited while migrating the group. Embeds {{ the number of invited users }}. */
|
||||
"GROUP_WAS_MIGRATED_USERS_INVITED_N_FORMAT" = "%@ members couldn’t be added to the New Group and have been invited to join.";
|
||||
|
||||
/* Message indicating that the local user left the group. */
|
||||
"GROUP_YOU_LEFT" = "You have left the group.";
|
||||
|
||||
@ -2053,6 +2068,18 @@
|
||||
/* Title for the 'legacy group' alert view. */
|
||||
"GROUPS_LEGACY_GROUP_ALERT_TITLE" = "Legacy vs. New Groups";
|
||||
|
||||
/* Title for the 'can't upgrade legacy group' alert view. */
|
||||
"GROUPS_LEGACY_GROUP_CANT_UPGRADE_ALERT_TITLE" = "Can’t Upgrade to New Group";
|
||||
|
||||
/* Text indicating that a legacy group can't be upgraded because it has too many members. Embeds {{ The maximum number of members allowed in a group. }}. */
|
||||
"GROUPS_LEGACY_GROUP_CANT_UPGRADE_ALERT_TOO_MANY_MEMBERS_FORMAT" = "This group can’t be upgraded to a New Group because it has too many members. The maximum group size that can be upgraded is %@.";
|
||||
|
||||
/* Explanation group migration for groups that can't yet be migrated in the 'legacy group' alert views. */
|
||||
"GROUPS_LEGACY_GROUP_CANT_UPGRADE_YET_1" = "When all current group members are using a version of Signal capable of joining a New Group, this group will be upgraded.";
|
||||
|
||||
/* Explanation group migration for groups that can't yet be migrated in the 'legacy group' alert views. */
|
||||
"GROUPS_LEGACY_GROUP_CANT_UPGRADE_YET_2" = "All message history and media will be kept from before the upgrade.";
|
||||
|
||||
/* Title for alert that explains that a new group will be a legacy group because 1 member does not support v2 groups. */
|
||||
"GROUPS_LEGACY_GROUP_CREATION_WARNING_ALERT_TITLE_1" = "A Legacy Group will be created because 1 member is using an old version of Signal. You can create a New Style Group with them after they update Signal, or remove them before creating the group.";
|
||||
|
||||
@ -2068,12 +2095,48 @@
|
||||
/* Brief explanation of legacy groups. Embeds {{ a \"learn more\" link. }}. */
|
||||
"GROUPS_LEGACY_GROUP_DESCRIPTION_FORMAT" = "This is a Legacy Group. Features like group admins are only available for New Groups. %@";
|
||||
|
||||
/* Indicates that a legacy group can't be upgraded because some members can't be migrated. Embeds {{ an \"learn more\" link. }}. */
|
||||
"GROUPS_LEGACY_GROUP_DESCRIPTION_MEMBERS_CANT_BE_MIGRATED_FORMAT" = "This Legacy Group is not yet ready to be upgraded to a New Group. %@.";
|
||||
|
||||
/* Indicates that a legacy group can't be upgraded because it has too many members. Embeds {{ an \"learn more\" link. }}. */
|
||||
"GROUPS_LEGACY_GROUP_DESCRIPTION_TOO_MANY_MEMBERS_FORMAT" = "This Legacy Group can’t be upgraded to a New Group because it is too large. %@.";
|
||||
|
||||
/* Explanation of legacy groups. Embeds {{ an \"upgrade\" link. }}. */
|
||||
"GROUPS_LEGACY_GROUP_DESCRIPTION_WITH_UPGRADE_OFFER_FORMAT" = "This is a Legacy Group. To access new features like @mentions and admins, %@.";
|
||||
|
||||
/* Label indicating a legacy group. */
|
||||
"GROUPS_LEGACY_GROUP_INDICATOR" = "Legacy Group";
|
||||
|
||||
/* A \"learn more\" link with more information about legacy groups. */
|
||||
"GROUPS_LEGACY_GROUP_LEARN_MORE_LINK" = "Learn More";
|
||||
|
||||
/* Explanation of new groups in the 'legacy group' alert views. */
|
||||
"GROUPS_LEGACY_GROUP_NEW_GROUP_DESCRIPTION" = "New Groups have features like @mentions and group admins, and will support more features in the future.";
|
||||
|
||||
/* Title for the 'new groups' alert view. */
|
||||
"GROUPS_LEGACY_GROUP_NEW_GROUPS_ALERT_TITLE" = "New Groups";
|
||||
|
||||
/* Body text for the second section of the 'upgrade legacy group' alert view. */
|
||||
"GROUPS_LEGACY_GROUP_UPGRADE_ALERT_SECTION_2_BODY" = "All message history and media has been kept from before the upgrade.";
|
||||
|
||||
/* Body text for the 'invites members' section of the 'upgrade legacy group' alert view. */
|
||||
"GROUPS_LEGACY_GROUP_UPGRADE_ALERT_SECTION_INVITED_MEMBERS" = "These members will need to accept an invite to join this group again, and will not receive group messages until they accept:";
|
||||
|
||||
/* Title for the 'upgrade legacy group' alert view. */
|
||||
"GROUPS_LEGACY_GROUP_UPGRADE_ALERT_TITLE" = "Upgrade to New Group";
|
||||
|
||||
/* Label for the 'upgrade this group' button in the 'upgrade legacy group' alert view. */
|
||||
"GROUPS_LEGACY_GROUP_UPGRADE_ALERT_UPGRADE_BUTTON" = "Upgrade this Group";
|
||||
|
||||
/* Error indicating the group update failed. */
|
||||
"GROUPS_LEGACY_GROUP_UPGRADE_ALERT_UPGRADE_FAILED_ERROR" = "Upgrade Failed. Please try again.";
|
||||
|
||||
/* Message indicating the group update succeeded. */
|
||||
"GROUPS_LEGACY_GROUP_UPGRADE_ALERT_UPGRADE_SUCCEEDED" = "Upgraded to New Group.";
|
||||
|
||||
/* An \"upgrade\" link for upgrading legacy groups to new groups. */
|
||||
"GROUPS_LEGACY_GROUP_UPGRADE_LINK" = "upgrade this group";
|
||||
|
||||
/* Message for the 'replace group admin' alert. */
|
||||
"GROUPS_REPLACE_ADMIN_ALERT_MESSAGE" = "Before you leave, choose a new admin for this group.";
|
||||
|
||||
|
||||
@ -7,6 +7,8 @@ import UIKit
|
||||
@objc
|
||||
open class ActionSheetController: OWSViewController {
|
||||
|
||||
private var isFullWidth = false
|
||||
|
||||
private let contentView = UIView()
|
||||
private let stackView = UIStackView()
|
||||
private let scrollView = UIScrollView()
|
||||
@ -62,7 +64,13 @@ open class ActionSheetController: OWSViewController {
|
||||
|
||||
@objc
|
||||
public convenience init(title: String? = nil, message: String? = nil) {
|
||||
self.init(title: title, message: message, isFullWidth: false)
|
||||
}
|
||||
|
||||
@objc
|
||||
public convenience init(title: String? = nil, message: String? = nil, isFullWidth: Bool = false) {
|
||||
self.init()
|
||||
self.isFullWidth = isFullWidth
|
||||
createHeader(title: title, message: message)
|
||||
}
|
||||
|
||||
@ -113,7 +121,12 @@ open class ActionSheetController: OWSViewController {
|
||||
scrollView.clipsToBounds = false
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
scrollView.autoHCenterInSuperview()
|
||||
if isFullWidth {
|
||||
scrollView.autoPinEdge(toSuperviewEdge: .leading)
|
||||
scrollView.autoPinEdge(toSuperviewEdge: .trailing)
|
||||
} else {
|
||||
scrollView.autoHCenterInSuperview()
|
||||
}
|
||||
scrollView.autoMatch(.height, to: .height, of: view, withOffset: 0, relation: .lessThanOrEqual)
|
||||
|
||||
// Prefer to be full width, but don't exceed the maximum width
|
||||
|
||||
@ -40,9 +40,8 @@ class MentionPicker: UIView {
|
||||
address: address,
|
||||
username: Self.profileManager.username(for: address, transaction: transaction),
|
||||
displayName: Self.contactsManager.displayName(for: address, transaction: transaction),
|
||||
conversationColorName: ConversationColorName(
|
||||
rawValue: TSContactThread.conversationColorName(forContactAddress: address, transaction: transaction)
|
||||
)
|
||||
conversationColorName: TSContactThread.conversationColorName(forContactAddress: address,
|
||||
transaction: transaction)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,7 +92,6 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification;
|
||||
transaction:(SDSAnyReadTransaction *)transaction;
|
||||
- (NSString *)comparableNameForAddress:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction;
|
||||
|
||||
- (nullable UIImage *)systemContactOrSyncedImageForAddress:(nullable SignalServiceAddress *)address;
|
||||
- (nullable UIImage *)profileImageForAddressWithSneakyTransaction:(nullable SignalServiceAddress *)address;
|
||||
- (nullable NSData *)profileImageDataForAddressWithSneakyTransaction:(nullable SignalServiceAddress *)address;
|
||||
- (nullable UIImage *)imageForAddress:(nullable SignalServiceAddress *)address
|
||||
|
||||
@ -1193,13 +1193,14 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan
|
||||
}
|
||||
|
||||
- (nullable UIImage *)systemContactOrSyncedImageForAddress:(nullable SignalServiceAddress *)address
|
||||
transaction:(SDSAnyReadTransaction *)transaction
|
||||
{
|
||||
if (address == nil) {
|
||||
OWSFailDebug(@"address was unexpectedly nil");
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *_Nullable phoneNumber = [self phoneNumberForAddress:address];
|
||||
NSString *_Nullable phoneNumber = [self phoneNumberForAddress:address transaction:transaction];
|
||||
Contact *_Nullable contact = self.allContactsMap[phoneNumber];
|
||||
|
||||
if (contact != nil && contact.cnContactId != nil) {
|
||||
@ -1210,7 +1211,7 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan
|
||||
}
|
||||
|
||||
// If we haven't loaded system contacts yet, we may have a cached copy in the db
|
||||
SignalAccount *signalAccount = [self fetchSignalAccountForAddress:address];
|
||||
SignalAccount *signalAccount = [self fetchSignalAccountForAddress:address transaction:transaction];
|
||||
if (signalAccount == nil) {
|
||||
return nil;
|
||||
}
|
||||
@ -1266,13 +1267,11 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan
|
||||
return nil;
|
||||
}
|
||||
|
||||
__block UIImage *_Nullable image = [self systemContactOrSyncedImageForAddress:address];
|
||||
if (image != nil) {
|
||||
return image;
|
||||
}
|
||||
|
||||
// Else try to use the image from their profile
|
||||
return [self profileImageForAddressWithSneakyTransaction:address];
|
||||
__block UIImage *_Nullable image;
|
||||
[self.databaseStorage readWithBlock:^(SDSAnyReadTransaction *transaction) {
|
||||
image = [self imageForAddress:address transaction:transaction];
|
||||
}];
|
||||
return image;
|
||||
}
|
||||
|
||||
- (nullable UIImage *)imageForAddress:(nullable SignalServiceAddress *)address
|
||||
@ -1283,7 +1282,7 @@ NSString *const OWSContactsManagerKeyNextFullIntersectionDate = @"OWSContactsMan
|
||||
return nil;
|
||||
}
|
||||
|
||||
__block UIImage *_Nullable image = [self systemContactOrSyncedImageForAddress:address];
|
||||
__block UIImage *_Nullable image = [self systemContactOrSyncedImageForAddress:address transaction:transaction];
|
||||
if (image != nil) {
|
||||
return image;
|
||||
}
|
||||
|
||||
@ -556,7 +556,7 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
|
||||
continue
|
||||
}
|
||||
|
||||
guard allMemberCount <= GroupManager.maxGroupsV2MemberCount else {
|
||||
guard allMemberCount <= GroupManager.groupsV2MaxGroupSizeHardLimit else {
|
||||
throw GroupsV2Error.tooManyMembers
|
||||
}
|
||||
|
||||
|
||||
@ -40,15 +40,28 @@ public class GroupsV2Migration {
|
||||
public static func v2MasterKey(forV1GroupId v1GroupId: Data) throws -> Data {
|
||||
try calculateMigrationMetadata(forV1GroupId: v1GroupId).v2MasterKey
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
public extension GroupsV2Migration {
|
||||
|
||||
// MARK: -
|
||||
|
||||
static func tryManualMigration(groupThread: TSGroupThread) -> Promise<TSGroupThread> {
|
||||
guard FeatureFlags.groupsV2MigrationManualMigrationPolite ||
|
||||
FeatureFlags.groupsV2MigrationManualMigrationAggressive else {
|
||||
return Promise(error: OWSAssertionError("Manual migration not enabled."))
|
||||
}
|
||||
return tryToMigrate(groupThread: groupThread, migrationMode: manualMigrationMode)
|
||||
}
|
||||
|
||||
// If there is a v1 group in the database that can be
|
||||
// migrated to a v2 group, try to migrate it to a v2
|
||||
// group. It might or might not already be migrated on
|
||||
// the service.
|
||||
public static func tryToMigrate(groupThread: TSGroupThread,
|
||||
migrationMode: GroupsV2MigrationMode) -> Promise<TSGroupThread> {
|
||||
static func tryToMigrate(groupThread: TSGroupThread,
|
||||
migrationMode: GroupsV2MigrationMode) -> Promise<TSGroupThread> {
|
||||
firstly(on: .global()) {
|
||||
Self.databaseStorage.read { transaction in
|
||||
Self.canGroupBeMigratedByLocalUser(groupThread: groupThread,
|
||||
@ -66,7 +79,7 @@ public class GroupsV2Migration {
|
||||
|
||||
// If there is a corresponding v1 group in the local database,
|
||||
// update it to reflect the v1 group already on the service.
|
||||
public static func updateAlreadyMigratedGroupIfNecessary(v2GroupId: Data) -> Promise<Void> {
|
||||
static func updateAlreadyMigratedGroupIfNecessary(v2GroupId: Data) -> Promise<Void> {
|
||||
firstly(on: .global()) { () -> Promise<Void> in
|
||||
guard let groupThread = (Self.databaseStorage.read { transaction in
|
||||
TSGroupThread.fetch(groupId: v2GroupId, transaction: transaction)
|
||||
@ -85,6 +98,45 @@ public class GroupsV2Migration {
|
||||
}.asVoid()
|
||||
}
|
||||
}
|
||||
|
||||
static func migrationInfoForManualMigration(groupThread: TSGroupThread) -> GroupsV2MigrationInfo {
|
||||
databaseStorage.read { transaction in
|
||||
migrationInfoForManualMigration(groupThread: groupThread,
|
||||
transaction: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
// Will return nil if the group cannot be migrated by the local
|
||||
// user for any reason.
|
||||
static func migrationInfoForManualMigration(groupThread: TSGroupThread,
|
||||
transaction: SDSAnyReadTransaction) -> GroupsV2MigrationInfo {
|
||||
|
||||
guard FeatureFlags.groupsV2MigrationManualMigrationPolite ||
|
||||
FeatureFlags.groupsV2MigrationManualMigrationAggressive else {
|
||||
return .buildCannotBeMigrated(state: .cantBeMigrated_FeatureNotEnabled)
|
||||
}
|
||||
return migrationInfo(groupThread: groupThread,
|
||||
migrationMode: manualMigrationMode,
|
||||
transaction: transaction)
|
||||
}
|
||||
|
||||
private static var manualMigrationMode: GroupsV2MigrationMode {
|
||||
owsAssertDebug(FeatureFlags.groupsV2MigrationManualMigrationPolite ||
|
||||
FeatureFlags.groupsV2MigrationManualMigrationAggressive)
|
||||
|
||||
return (FeatureFlags.groupsV2MigrationManualMigrationAggressive
|
||||
? .manualMigrationAggressive
|
||||
: .manualMigrationPolite)
|
||||
}
|
||||
|
||||
private static var autoMigrationMode: GroupsV2MigrationMode {
|
||||
owsAssertDebug(FeatureFlags.groupsV2MigrationAutoMigrationPolite ||
|
||||
FeatureFlags.groupsV2MigrationAutoMigrationAggressive)
|
||||
|
||||
return (FeatureFlags.groupsV2MigrationAutoMigrationAggressive
|
||||
? .manualMigrationAggressive
|
||||
: .manualMigrationPolite)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
@ -99,6 +151,12 @@ fileprivate extension GroupsV2Migration {
|
||||
return GroupManager.ensureLocalProfileHasCommitmentIfNecessary()
|
||||
}.map(on: .global()) { () -> UnmigratedState in
|
||||
try Self.loadUnmigratedState(groupId: groupId)
|
||||
}.then(on: .global()) { (unmigratedState: UnmigratedState) -> Promise<UnmigratedState> in
|
||||
firstly {
|
||||
Self.tryToFillInMissingUuids(unmigratedState: unmigratedState)
|
||||
}.map(on: .global()) {
|
||||
return unmigratedState
|
||||
}
|
||||
}.then(on: .global()) { (unmigratedState: UnmigratedState) -> Promise<TSGroupThread> in
|
||||
addMigratingV2GroupId(unmigratedState.migrationMetadata.v1GroupId)
|
||||
addMigratingV2GroupId(unmigratedState.migrationMetadata.v2GroupId)
|
||||
@ -120,6 +178,25 @@ fileprivate extension GroupsV2Migration {
|
||||
}
|
||||
}
|
||||
|
||||
static func tryToFillInMissingUuids(unmigratedState: UnmigratedState) -> Promise<Void> {
|
||||
let groupMembership = unmigratedState.groupThread.groupModel.groupMembership
|
||||
let membersToMigrate = membersToTryToMigrate(groupMembership: groupMembership)
|
||||
let phoneNumbersWithoutUuids = membersToMigrate.compactMap { (address: SignalServiceAddress) -> String? in
|
||||
if address.uuid != nil {
|
||||
return nil
|
||||
}
|
||||
return address.phoneNumber
|
||||
}
|
||||
guard !phoneNumbersWithoutUuids.isEmpty else {
|
||||
return Promise.value(())
|
||||
}
|
||||
|
||||
Logger.info("Trying to fill in missing uuids: \(phoneNumbersWithoutUuids.count)")
|
||||
|
||||
let discoveryTask = ContactDiscoveryTask(phoneNumbers: Set(phoneNumbersWithoutUuids))
|
||||
return discoveryTask.perform().asVoid()
|
||||
}
|
||||
|
||||
static func attemptToMigrateByPullingFromService(unmigratedState: UnmigratedState,
|
||||
migrationMode: GroupsV2MigrationMode) -> Promise<TSGroupThread> {
|
||||
|
||||
@ -190,12 +267,12 @@ fileprivate extension GroupsV2Migration {
|
||||
guard groupThread.isLocalUserFullMember else {
|
||||
throw OWSAssertionError("Local user cannot migrate group; is not a full member.")
|
||||
}
|
||||
let memberSet = groupThread.groupMembership.allMembersOfAnyKind
|
||||
let membersToMigrate = membersToTryToMigrate(groupMembership: groupThread.groupMembership)
|
||||
|
||||
return firstly(on: .global()) { () -> Promise<Void> in
|
||||
GroupManager.tryToEnableGroupsV2(for: Array(memberSet), isBlocking: true, ignoreErrors: true)
|
||||
GroupManager.tryToEnableGroupsV2(for: Array(membersToMigrate), isBlocking: true, ignoreErrors: true)
|
||||
}.then(on: .global()) { () throws -> Promise<Void> in
|
||||
self.groupsV2.tryToEnsureProfileKeyCredentials(for: Array(memberSet))
|
||||
self.groupsV2.tryToEnsureProfileKeyCredentials(for: Array(membersToMigrate))
|
||||
}.then(on: .global()) { () throws -> Promise<String?> in
|
||||
guard let avatarData = unmigratedState.groupThread.groupModel.groupAvatarData else {
|
||||
// No avatar to upload.
|
||||
@ -284,32 +361,32 @@ fileprivate extension GroupsV2Migration {
|
||||
// The group creator is an administrator;
|
||||
// the other members are normal users.
|
||||
var v2MembershipBuilder = GroupMembership.Builder()
|
||||
for address in v1GroupModel.groupMembership.allMembersOfAnyKind {
|
||||
let membersToMigrate = membersToTryToMigrate(groupMembership: v1GroupModel.groupMembership)
|
||||
for address in membersToMigrate {
|
||||
guard address.uuid != nil else {
|
||||
Logger.warn("Member missing uuid: \(address).")
|
||||
if migrationMode.skipMembersWithoutUuids {
|
||||
Logger.warn("Discarding member: \(address).")
|
||||
continue
|
||||
} else {
|
||||
throw OWSAssertionError("Member does not support gv2.")
|
||||
}
|
||||
owsAssertDebug(migrationMode.canSkipMembersWithoutUuids)
|
||||
Logger.warn("Discarding member: \(address).")
|
||||
continue
|
||||
}
|
||||
|
||||
if !GroupManager.doesUserHaveGroupsV2Capability(address: address,
|
||||
transaction: transaction) {
|
||||
Logger.warn("Member without Groups v2 capability: \(address).")
|
||||
owsAssertDebug(migrationMode.allowMembersWithoutCapabilities)
|
||||
owsAssertDebug(migrationMode.canSkipMembersWithoutCapabilities)
|
||||
continue
|
||||
}
|
||||
if !GroupManager.doesUserHaveGroupsV2MigrationCapability(address: address,
|
||||
transaction: transaction) {
|
||||
Logger.warn("Member without migration capability: \(address).")
|
||||
owsAssertDebug(migrationMode.allowMembersWithoutCapabilities)
|
||||
owsAssertDebug(migrationMode.canSkipMembersWithoutCapabilities)
|
||||
continue
|
||||
}
|
||||
|
||||
var isInvited = false
|
||||
if !groupsV2.hasProfileKeyCredential(for: address, transaction: transaction) {
|
||||
Logger.warn("Inviting user with unknown profile key: \(address).")
|
||||
owsAssertDebug(migrationMode.allowMembersWithoutProfileKey)
|
||||
owsAssertDebug(migrationMode.canInviteMembersWithoutProfileKey)
|
||||
isInvited = true
|
||||
}
|
||||
|
||||
@ -360,96 +437,231 @@ fileprivate extension GroupsV2Migration {
|
||||
static func canGroupBeMigratedByLocalUser(groupThread: TSGroupThread,
|
||||
migrationMode: GroupsV2MigrationMode,
|
||||
transaction: SDSAnyReadTransaction) -> Bool {
|
||||
if migrationMode.onlyMigrateIfInProfileWhitelist {
|
||||
guard profileManager.isThread(inProfileWhitelist: groupThread,
|
||||
transaction: transaction) else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
migrationInfo(groupThread: groupThread,
|
||||
migrationMode: migrationMode,
|
||||
transaction: transaction).canGroupBeMigrated
|
||||
}
|
||||
|
||||
// This method might be called for any group (v1 or v2).
|
||||
// It returns a description of whether the group can be
|
||||
// migrated, and if so under what conditions.
|
||||
//
|
||||
// Will return nil if the group cannot be migrated by the local
|
||||
// user for any reason.
|
||||
static func migrationInfo(groupThread: TSGroupThread,
|
||||
migrationMode: GroupsV2MigrationMode,
|
||||
transaction: SDSAnyReadTransaction) -> GroupsV2MigrationInfo {
|
||||
|
||||
guard groupThread.isGroupV1Thread else {
|
||||
owsFailDebug("Not a v1 group.")
|
||||
return false
|
||||
return .buildCannotBeMigrated(state: .cantBeMigrated_NotAV1Group)
|
||||
}
|
||||
guard groupThread.isLocalUserFullMember else {
|
||||
return false
|
||||
}
|
||||
let groupMembership = groupThread.groupModel.groupMembership
|
||||
let isLocalUserFullMember = groupThread.isLocalUserFullMember
|
||||
|
||||
guard let localAddress = tsAccountManager.localAddress else {
|
||||
owsFailDebug("Missing localAddress.")
|
||||
return false
|
||||
return .buildCannotBeMigrated(state: .cantBeMigrated_NotRegistered)
|
||||
}
|
||||
|
||||
// Build member list.
|
||||
let isGroupInProfileWhitelist = profileManager.isThread(inProfileWhitelist: groupThread,
|
||||
transaction: transaction)
|
||||
|
||||
let groupMembership = groupThread.groupModel.groupMembership
|
||||
let membersToMigrate = membersToTryToMigrate(groupMembership: groupMembership)
|
||||
|
||||
// Inspect member list.
|
||||
//
|
||||
// The group creator is an administrator;
|
||||
// the other members are normal users.
|
||||
for address in groupMembership.allMembersOfAnyKind {
|
||||
var membersWithoutUuids = [SignalServiceAddress]()
|
||||
var membersWithoutCapabilities = [SignalServiceAddress]()
|
||||
var membersWithoutProfileKeys = [SignalServiceAddress]()
|
||||
var membersMigrated = [SignalServiceAddress]()
|
||||
for address in membersToMigrate {
|
||||
if address.isEqualToAddress(localAddress) {
|
||||
continue
|
||||
}
|
||||
|
||||
guard nil != address.uuid else {
|
||||
Logger.warn("Member missing uuid: \(address).")
|
||||
if migrationMode.skipMembersWithoutUuids {
|
||||
continue
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
Logger.warn("Member without uuid: \(address).")
|
||||
membersWithoutUuids.append(address)
|
||||
continue
|
||||
}
|
||||
|
||||
if !GroupManager.doesUserHaveGroupsV2Capability(address: address,
|
||||
transaction: transaction) {
|
||||
Logger.warn("Member without Groups v2 capability: \(address).")
|
||||
if !migrationMode.allowMembersWithoutCapabilities {
|
||||
return false
|
||||
}
|
||||
membersWithoutCapabilities.append(address)
|
||||
continue
|
||||
}
|
||||
if !GroupManager.doesUserHaveGroupsV2MigrationCapability(address: address,
|
||||
transaction: transaction) {
|
||||
Logger.warn("Member without migration capability: \(address).")
|
||||
if !migrationMode.allowMembersWithoutCapabilities {
|
||||
return false
|
||||
}
|
||||
membersWithoutCapabilities.append(address)
|
||||
continue
|
||||
}
|
||||
|
||||
membersMigrated.append(address)
|
||||
|
||||
if !groupsV2.hasProfileKeyCredential(for: address, transaction: transaction) {
|
||||
Logger.warn("Member without profile key: \(address).")
|
||||
if !migrationMode.allowMembersWithoutProfileKey {
|
||||
return false
|
||||
}
|
||||
membersWithoutProfileKeys.append(address)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
let hasTooManyMembers = membersMigrated.count > GroupManager.groupsV2MaxGroupSizeHardLimit
|
||||
|
||||
let state: GroupsV2MigrationState = {
|
||||
if !migrationMode.canMigrateIfNotMember,
|
||||
!isLocalUserFullMember {
|
||||
return .cantBeMigrated_LocalUserIsNotAMember
|
||||
}
|
||||
if !migrationMode.canMigrateIfNotInProfileWhitelist,
|
||||
!isGroupInProfileWhitelist {
|
||||
return .cantBeMigrated_NotInProfileWhitelist
|
||||
}
|
||||
if !migrationMode.canSkipMembersWithoutUuids,
|
||||
!membersWithoutUuids.isEmpty {
|
||||
return .cantBeMigrated_MembersWithoutUuids
|
||||
}
|
||||
if !migrationMode.canSkipMembersWithoutCapabilities,
|
||||
!membersWithoutCapabilities.isEmpty {
|
||||
return .cantBeMigrated_MembersWithoutCapabilities
|
||||
}
|
||||
if !migrationMode.canInviteMembersWithoutProfileKey,
|
||||
!membersWithoutProfileKeys.isEmpty {
|
||||
return .cantBeMigrated_MembersWithoutProfileKey
|
||||
}
|
||||
if !migrationMode.canMigrateWithTooManyMembers,
|
||||
hasTooManyMembers {
|
||||
return .cantBeMigrated_TooManyMembers
|
||||
}
|
||||
return .canBeMigrated
|
||||
}()
|
||||
|
||||
return GroupsV2MigrationInfo(isGroupInProfileWhitelist: isGroupInProfileWhitelist,
|
||||
membersWithoutUuids: membersWithoutUuids,
|
||||
membersWithoutCapabilities: membersWithoutCapabilities,
|
||||
membersWithoutProfileKeys: membersWithoutProfileKeys,
|
||||
state: state)
|
||||
}
|
||||
|
||||
static func membersToTryToMigrate(groupMembership: GroupMembership) -> Set<SignalServiceAddress> {
|
||||
|
||||
let allMembers = groupMembership.allMembersOfAnyKind
|
||||
let addressesWithoutUuids = Array(allMembers).filter { $0.uuid == nil }
|
||||
let knownUndiscoverable = Set(ContactDiscoveryTask.addressesRecentlyMarkedAsUndiscoverableForGroupMigrations(addressesWithoutUuids))
|
||||
|
||||
var result = Set<SignalServiceAddress>()
|
||||
for address in allMembers {
|
||||
if nil == address.uuid, knownUndiscoverable.contains(address) {
|
||||
Logger.warn("Ignoring unregistered member without uuid: \(address).")
|
||||
continue
|
||||
}
|
||||
result.insert(address)
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
public enum GroupsV2MigrationState {
|
||||
case canBeMigrated
|
||||
case cantBeMigrated_FeatureNotEnabled
|
||||
case cantBeMigrated_NotAV1Group
|
||||
case cantBeMigrated_NotRegistered
|
||||
case cantBeMigrated_LocalUserIsNotAMember
|
||||
case cantBeMigrated_NotInProfileWhitelist
|
||||
case cantBeMigrated_TooManyMembers
|
||||
case cantBeMigrated_MembersWithoutUuids
|
||||
case cantBeMigrated_MembersWithoutCapabilities
|
||||
case cantBeMigrated_MembersWithoutProfileKey
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
public struct GroupsV2MigrationInfo {
|
||||
// These properties only have valid values if canGroupBeMigrated is true.
|
||||
public let isGroupInProfileWhitelist: Bool
|
||||
public let membersWithoutUuids: [SignalServiceAddress]
|
||||
public let membersWithoutCapabilities: [SignalServiceAddress]
|
||||
public let membersWithoutProfileKeys: [SignalServiceAddress]
|
||||
|
||||
// Always consult this property first.
|
||||
public let state: GroupsV2MigrationState
|
||||
|
||||
public var canGroupBeMigrated: Bool {
|
||||
state == .canBeMigrated
|
||||
}
|
||||
|
||||
fileprivate static func buildCannotBeMigrated(state: GroupsV2MigrationState) -> GroupsV2MigrationInfo {
|
||||
GroupsV2MigrationInfo(isGroupInProfileWhitelist: false,
|
||||
membersWithoutUuids: [],
|
||||
membersWithoutCapabilities: [],
|
||||
membersWithoutProfileKeys: [],
|
||||
state: state)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
public enum GroupsV2MigrationMode {
|
||||
case migrateToServicePolite
|
||||
case migrateToServiceAggressive
|
||||
// TODO: We may want to rename polite/aggressive to
|
||||
// auto-migration/manual-migration.
|
||||
case manualMigrationPolite
|
||||
case manualMigrationAggressive
|
||||
case autoMigrationPolite
|
||||
case autoMigrationAggressive
|
||||
case alreadyMigratedOnService
|
||||
|
||||
public var skipMembersWithoutUuids: Bool {
|
||||
self == .migrateToServiceAggressive
|
||||
private var isManualMigration: Bool {
|
||||
self == .manualMigrationPolite || self == .manualMigrationAggressive
|
||||
}
|
||||
|
||||
public var allowMembersWithoutCapabilities: Bool {
|
||||
self == .migrateToServiceAggressive
|
||||
private var isAutoMigration: Bool {
|
||||
self == .autoMigrationPolite || self == .autoMigrationAggressive
|
||||
}
|
||||
|
||||
public var allowMembersWithoutProfileKey: Bool {
|
||||
self == .migrateToServiceAggressive
|
||||
private var isPolite: Bool {
|
||||
self == .manualMigrationPolite || self == .autoMigrationPolite
|
||||
}
|
||||
|
||||
public var onlyMigrateIfInProfileWhitelist: Bool {
|
||||
self == .migrateToServiceAggressive
|
||||
private var isAggressive: Bool {
|
||||
self == .manualMigrationAggressive || self == .autoMigrationAggressive
|
||||
}
|
||||
|
||||
private var isAlreadyMigratedOnService: Bool {
|
||||
self == .alreadyMigratedOnService
|
||||
}
|
||||
|
||||
public var canSkipMembersWithoutUuids: Bool {
|
||||
isAggressive || isAlreadyMigratedOnService
|
||||
}
|
||||
|
||||
public var canSkipMembersWithoutCapabilities: Bool {
|
||||
isAggressive || isAlreadyMigratedOnService
|
||||
}
|
||||
|
||||
public var canInviteMembersWithoutProfileKey: Bool {
|
||||
isManualMigration || isAggressive || isAlreadyMigratedOnService
|
||||
}
|
||||
|
||||
public var canMigrateIfNotInProfileWhitelist: Bool {
|
||||
// TODO: What about manual?
|
||||
isAlreadyMigratedOnService
|
||||
}
|
||||
|
||||
public var canMigrateToService: Bool {
|
||||
self != .alreadyMigratedOnService
|
||||
!isAlreadyMigratedOnService
|
||||
}
|
||||
|
||||
public var canMigrateIfNotMember: Bool {
|
||||
isAlreadyMigratedOnService
|
||||
}
|
||||
|
||||
public var canMigrateWithTooManyMembers: Bool {
|
||||
isAlreadyMigratedOnService
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1946,7 +1946,7 @@ const NSString *kNSNotificationKey_WasLocallyInitiated = @"kNSNotificationKey_Wa
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
ActionSheetController *actionSheet = [[ActionSheetController alloc] initWithTitle:nil message:nil];
|
||||
ActionSheetController *actionSheet = [[ActionSheetController alloc] init];
|
||||
|
||||
NSString *shareTitle = NSLocalizedString(@"CONVERSATION_SETTINGS_VIEW_SHARE_PROFILE",
|
||||
@"Button to confirm that user wants to share their profile with a user or group.");
|
||||
|
||||
@ -9,6 +9,7 @@ extern const NSUInteger kStandardAvatarSize;
|
||||
extern const NSUInteger kMediumAvatarSize;
|
||||
extern const NSUInteger kLargeAvatarSize;
|
||||
|
||||
@class SDSAnyReadTransaction;
|
||||
@class TSThread;
|
||||
@class UIImage;
|
||||
|
||||
@ -20,8 +21,10 @@ extern const NSUInteger kLargeAvatarSize;
|
||||
+ (nullable UIImage *)buildRandomAvatarWithDiameter:(NSUInteger)diameter;
|
||||
|
||||
- (nullable UIImage *)buildSavedImage;
|
||||
- (nullable UIImage *)buildSavedImageWithTransaction:(SDSAnyReadTransaction *)transaction;
|
||||
- (nullable UIImage *)buildDefaultImage;
|
||||
- (nullable UIImage *)build;
|
||||
- (nullable UIImage *)buildWithTransaction:(SDSAnyReadTransaction *)transaction;
|
||||
|
||||
+ (nullable UIImage *)avatarImageWithInitials:(NSString *)initials
|
||||
backgroundColor:(UIColor *)backgroundColor
|
||||
|
||||
@ -285,12 +285,28 @@ typedef void (^OWSAvatarDrawBlock)(CGContextRef context);
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable UIImage *)buildWithTransaction:(SDSAnyReadTransaction *)transaction
|
||||
{
|
||||
UIImage *_Nullable savedImage = [self buildSavedImageWithTransaction:transaction];
|
||||
if (savedImage) {
|
||||
return savedImage;
|
||||
} else {
|
||||
return [self buildDefaultImage];
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable UIImage *)buildSavedImage
|
||||
{
|
||||
OWSAbstractMethod();
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (nullable UIImage *)buildSavedImageWithTransaction:(SDSAnyReadTransaction *)transaction
|
||||
{
|
||||
OWSAbstractMethod();
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (nullable UIImage *)buildDefaultImage
|
||||
{
|
||||
OWSAbstractMethod();
|
||||
|
||||
@ -19,6 +19,11 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
colorName:(ConversationColorName)colorName
|
||||
diameter:(NSUInteger)diameter;
|
||||
|
||||
- (instancetype)initWithAddress:(SignalServiceAddress *)address
|
||||
colorName:(ConversationColorName)colorName
|
||||
diameter:(NSUInteger)diameter
|
||||
transaction:(SDSAnyReadTransaction *)transaction;
|
||||
|
||||
/**
|
||||
* Build an avatar for a non-Signal recipient
|
||||
*/
|
||||
|
||||
@ -56,6 +56,17 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return [self initWithAddress:address nameComponents:nameComponents colorName:colorName diameter:diameter];
|
||||
}
|
||||
|
||||
- (instancetype)initWithAddress:(SignalServiceAddress *)address
|
||||
colorName:(ConversationColorName)colorName
|
||||
diameter:(NSUInteger)diameter
|
||||
transaction:(SDSAnyReadTransaction *)transaction
|
||||
{
|
||||
// Components for avatar initials.
|
||||
NSPersonNameComponents *_Nullable nameComponents =
|
||||
[OWSContactAvatarBuilder.contactsManager nameComponentsForAddress:address transaction:transaction];
|
||||
return [self initWithAddress:address nameComponents:nameComponents colorName:colorName diameter:diameter];
|
||||
}
|
||||
|
||||
- (instancetype)initWithNonSignalNameComponents:(NSPersonNameComponents *)nonSignalNameComponents
|
||||
colorSeed:(NSString *)colorSeed
|
||||
diameter:(NSUInteger)diameter
|
||||
@ -90,29 +101,48 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
}
|
||||
|
||||
if (self.address.isLocalAddress) {
|
||||
NSString *noteToSelfCacheKey = [NSString stringWithFormat:@"%@:note-to-self", self.cacheKey];
|
||||
UIImage *_Nullable cachedAvatar =
|
||||
[OWSContactAvatarBuilder.contactsManager.avatarCache imageForKey:noteToSelfCacheKey
|
||||
diameter:(CGFloat)self.diameter];
|
||||
if (cachedAvatar) {
|
||||
return cachedAvatar;
|
||||
}
|
||||
|
||||
UIImage *image = [self noteToSelfImageWithConversationColorName:self.colorName diameter:(CGFloat)self.diameter];
|
||||
if (!image) {
|
||||
OWSFailDebug(@"Could not generate avatar.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
[OWSContactAvatarBuilder.contactsManager.avatarCache setImage:image
|
||||
forKey:noteToSelfCacheKey
|
||||
diameter:self.diameter];
|
||||
return image;
|
||||
return self.buildImageForLocalUser;
|
||||
}
|
||||
|
||||
return [OWSContactAvatarBuilder.contactsManager imageForAddressWithSneakyTransaction:self.address];
|
||||
}
|
||||
|
||||
- (nullable UIImage *)buildSavedImageWithTransaction:(SDSAnyReadTransaction *)transaction
|
||||
{
|
||||
if (!self.address.isValid) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (self.address.isLocalAddress) {
|
||||
return self.buildImageForLocalUser;
|
||||
}
|
||||
|
||||
return [OWSContactAvatarBuilder.contactsManager imageForAddress:self.address transaction:transaction];
|
||||
}
|
||||
|
||||
- (nullable UIImage *)buildImageForLocalUser
|
||||
{
|
||||
OWSCAssertDebug(self.address.isLocalAddress);
|
||||
NSString *noteToSelfCacheKey = [NSString stringWithFormat:@"%@:note-to-self", self.cacheKey];
|
||||
UIImage *_Nullable cachedAvatar =
|
||||
[OWSContactAvatarBuilder.contactsManager.avatarCache imageForKey:noteToSelfCacheKey
|
||||
diameter:(CGFloat)self.diameter];
|
||||
if (cachedAvatar) {
|
||||
return cachedAvatar;
|
||||
}
|
||||
|
||||
UIImage *image = [self noteToSelfImageWithConversationColorName:self.colorName diameter:(CGFloat)self.diameter];
|
||||
if (!image) {
|
||||
OWSFailDebug(@"Could not generate avatar.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
[OWSContactAvatarBuilder.contactsManager.avatarCache setImage:image
|
||||
forKey:noteToSelfCacheKey
|
||||
diameter:self.diameter];
|
||||
return image;
|
||||
}
|
||||
|
||||
- (id)cacheKey
|
||||
{
|
||||
if (self.address.isValid) {
|
||||
|
||||
@ -47,6 +47,11 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
return self.thread.groupModel.groupAvatarImage;
|
||||
}
|
||||
|
||||
- (nullable UIImage *)buildSavedImageWithTransaction:(SDSAnyReadTransaction *)transaction
|
||||
{
|
||||
return self.thread.groupModel.groupAvatarImage;
|
||||
}
|
||||
|
||||
- (nullable UIImage *)buildDefaultImage
|
||||
{
|
||||
return [self.class defaultAvatarForGroupId:self.thread.groupModel.groupId
|
||||
|
||||
@ -199,10 +199,18 @@ public extension ContactDiscoveryTask {
|
||||
}
|
||||
}
|
||||
|
||||
static func addressesRecentlyMarkedAsUndiscoverable(_ addresses: [SignalServiceAddress]) -> [SignalServiceAddress] {
|
||||
static func addressesRecentlyMarkedAsUndiscoverableForMessageSends(_ addresses: [SignalServiceAddress]) -> [SignalServiceAddress] {
|
||||
guard FeatureFlags.ignoreCDSUndiscoverableUsersInMessageSends else {
|
||||
return []
|
||||
}
|
||||
return addressesRecentlyMarkedAsUndiscoverable(addresses)
|
||||
}
|
||||
|
||||
static func addressesRecentlyMarkedAsUndiscoverableForGroupMigrations(_ addresses: [SignalServiceAddress]) -> [SignalServiceAddress] {
|
||||
return addressesRecentlyMarkedAsUndiscoverable(addresses)
|
||||
}
|
||||
|
||||
private static func addressesRecentlyMarkedAsUndiscoverable(_ addresses: [SignalServiceAddress]) -> [SignalServiceAddress] {
|
||||
return unfairLock.withLock {
|
||||
addresses.filter { address in
|
||||
guard let phoneNumber = address.phoneNumber else {
|
||||
|
||||
@ -81,8 +81,8 @@ NS_DESIGNATED_INITIALIZER NS_SWIFT_NAME(init(grdbId:uniqueId:conversationColorNa
|
||||
// This method can be used to get the conversation color for a given
|
||||
// recipient without using a read/write transaction to create a
|
||||
// contact thread.
|
||||
+ (NSString *)conversationColorNameForContactAddress:(SignalServiceAddress *)address
|
||||
transaction:(SDSAnyReadTransaction *)transaction;
|
||||
+ (ConversationColorName)conversationColorNameForContactAddress:(SignalServiceAddress *)address
|
||||
transaction:(SDSAnyReadTransaction *)transaction;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@ -228,8 +228,8 @@ lastVisibleSortIdOnScreenPercentage:lastVisibleSortIdOnScreenPercentage
|
||||
return [threadId substringWithRange:NSMakeRange(1, threadId.length - 1)];
|
||||
}
|
||||
|
||||
+ (NSString *)conversationColorNameForContactAddress:(SignalServiceAddress *)address
|
||||
transaction:(SDSAnyReadTransaction *)transaction
|
||||
+ (ConversationColorName)conversationColorNameForContactAddress:(SignalServiceAddress *)address
|
||||
transaction:(SDSAnyReadTransaction *)transaction
|
||||
{
|
||||
OWSAssertDebug(address);
|
||||
|
||||
|
||||
@ -38,6 +38,9 @@ public enum GroupUpdateType: Int {
|
||||
case disappearingMessagesState_disabled
|
||||
case groupInviteLink
|
||||
case generic
|
||||
case groupMigrated
|
||||
case groupMigrated_usersDropped
|
||||
case groupMigrated_usersInvited
|
||||
case debug
|
||||
|
||||
var typeForDeduplication: GroupUpdateType {
|
||||
@ -161,6 +164,8 @@ struct GroupUpdateCopy {
|
||||
if isReplacingJoinRequestPlaceholder {
|
||||
addMembershipUpdates(oldGroupMembership: oldGroupMembership,
|
||||
forLocalUserOnly: true)
|
||||
} else if wasJustMigrated {
|
||||
addMigrationUpdates(oldGroupMembership: oldGroupMembership)
|
||||
} else {
|
||||
addMembershipUpdates(oldGroupMembership: oldGroupMembership)
|
||||
|
||||
@ -648,14 +653,20 @@ extension GroupUpdateCopy {
|
||||
}
|
||||
|
||||
mutating func addUserWasGrantedAdministrator(for address: SignalServiceAddress) {
|
||||
|
||||
if let newGroupModelV2 = newGroupModel as? TSGroupModelV2,
|
||||
newGroupModelV2.wasJustMigrated {
|
||||
// All v1 group members become admins when the
|
||||
// group is migrated to v2. We don't need to
|
||||
// surface this to the user.
|
||||
return
|
||||
}
|
||||
|
||||
let isLocalUser = localAddress == address
|
||||
if isLocalUser {
|
||||
switch updater {
|
||||
case .localUser:
|
||||
if let newGroupModelV2 = newGroupModel as? TSGroupModelV2,
|
||||
newGroupModelV2.wasJustMigrated {
|
||||
// Do nothing.
|
||||
} else if !DebugFlags.permissiveGroupUpdateInfoMessages {
|
||||
if !DebugFlags.permissiveGroupUpdateInfoMessages {
|
||||
owsFailDebug("Local user made themself administrator.")
|
||||
} else {
|
||||
addItem(.debug, copy: "Error: Local user made themself administrator.")
|
||||
@ -1062,7 +1073,7 @@ extension GroupUpdateCopy {
|
||||
guard count > 0 else {
|
||||
return
|
||||
}
|
||||
let countString = String.localizedStringWithFormat("%d", count)
|
||||
let countString = OWSFormat.formatUInt(count)
|
||||
|
||||
switch updater {
|
||||
case .localUser:
|
||||
@ -1272,7 +1283,7 @@ extension GroupUpdateCopy {
|
||||
}
|
||||
addUnnamedUsersWereInvitedPassiveTense(count: count)
|
||||
case .otherUser(let updaterName, _):
|
||||
let countString = String.localizedStringWithFormat("%d", count)
|
||||
let countString = OWSFormat.formatUInt(count)
|
||||
if count == 1 {
|
||||
let format = NSLocalizedString("GROUP_REMOTE_USER_INVITED_BY_REMOTE_USER_1_FORMAT",
|
||||
comment: "Message indicating that a single remote user was invited to the group by the local user. Embeds {{ user who invited the user }}.")
|
||||
@ -1290,7 +1301,7 @@ extension GroupUpdateCopy {
|
||||
}
|
||||
|
||||
mutating func addUnnamedUsersWereInvitedPassiveTense(count: UInt) {
|
||||
let countString = String.localizedStringWithFormat("%d", count)
|
||||
let countString = OWSFormat.formatUInt(count)
|
||||
if count == 1 {
|
||||
self.addItem(.userMembershipState_invitesNew,
|
||||
copy: NSLocalizedString("GROUP_REMOTE_USER_INVITED_1",
|
||||
@ -1678,6 +1689,58 @@ extension GroupUpdateCopy {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Migration
|
||||
|
||||
var wasJustMigrated: Bool {
|
||||
guard let newGroupModelV2 = newGroupModel as? TSGroupModelV2,
|
||||
newGroupModelV2.wasJustMigrated else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
mutating func addMigrationUpdates(oldGroupMembership: GroupMembership) {
|
||||
owsAssertDebug(wasJustMigrated)
|
||||
|
||||
addItem(.groupMigrated,
|
||||
copy: NSLocalizedString("GROUP_WAS_MIGRATED",
|
||||
comment: "Message indicating that the group was migrated."))
|
||||
|
||||
let invitedMembers = oldGroupMembership.allMembersOfAnyKind.intersection(newGroupMembership.invitedMembers)
|
||||
let droppedMembers = oldGroupMembership.allMembersOfAnyKind.subtracting(newGroupMembership.allMembersOfAnyKind)
|
||||
|
||||
if !invitedMembers.isEmpty {
|
||||
if invitedMembers.count == 1 {
|
||||
let copy = NSLocalizedString("GROUP_WAS_MIGRATED_USERS_INVITED_1",
|
||||
comment: "Message indicating that 1 user was invited while migrating the group.")
|
||||
addItem(.groupMigrated_usersDropped,
|
||||
copy: copy)
|
||||
} else {
|
||||
let format = NSLocalizedString("GROUP_WAS_MIGRATED_USERS_INVITED_N_FORMAT",
|
||||
comment: "Message indicating that N users were invited while migrating the group. Embeds {{ the number of invited users }}.")
|
||||
let countString = OWSFormat.formatInt(invitedMembers.count)
|
||||
addItem(.groupMigrated_usersInvited,
|
||||
format: format, countString)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We need final copy here.
|
||||
if !droppedMembers.isEmpty {
|
||||
if droppedMembers.count == 1 {
|
||||
let copy = NSLocalizedString("GROUP_WAS_MIGRATED_USERS_DROPPED_1",
|
||||
comment: "Message indicating that 1 user was dropped while migrating the group.")
|
||||
addItem(.groupMigrated_usersDropped,
|
||||
copy: copy)
|
||||
} else {
|
||||
let format = NSLocalizedString("GROUP_WAS_MIGRATED_USERS_DROPPED_N_FORMAT",
|
||||
comment: "Message indicating that N users were dropped while migrating the group. Embeds {{ the number of dropped users }}.")
|
||||
let countString = OWSFormat.formatInt(droppedMembers.count)
|
||||
addItem(.groupMigrated_usersDropped,
|
||||
format: format, countString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Membership Status
|
||||
|
||||
fileprivate enum MembershipStatus {
|
||||
|
||||
@ -684,7 +684,7 @@ extension MessageSender {
|
||||
return Promise.value(addresses)
|
||||
}
|
||||
|
||||
let knownUndiscoverable = ContactDiscoveryTask.addressesRecentlyMarkedAsUndiscoverable(invalidRecipients)
|
||||
let knownUndiscoverable = ContactDiscoveryTask.addressesRecentlyMarkedAsUndiscoverableForMessageSends(invalidRecipients)
|
||||
if Set(knownUndiscoverable) == Set(invalidRecipients) {
|
||||
// If CDS has recently indicated that all of the invalid recipients are undiscoverable,
|
||||
// assume they are still undiscoverable and skip them.
|
||||
|
||||
@ -155,25 +155,53 @@ public class FeatureFlags: BaseFlags {
|
||||
@objc
|
||||
public static let groupsV2showSplash = build.includes(.qa)
|
||||
|
||||
// TODO: Make this a remote config flag?
|
||||
@objc
|
||||
public static let groupsV2MigrationMigration = build.includes(.dev)
|
||||
public static let groupsV2Migration = build.includes(.dev)
|
||||
|
||||
@objc
|
||||
public static let groupsV2MigrationSetCapability = groupsV2MigrationMigration && false
|
||||
public static let groupsV2MigrationSetCapability = groupsV2Migration && build.includes(.dev)
|
||||
|
||||
@objc
|
||||
public static let groupsV2MigrationRequireCapability = false
|
||||
|
||||
// Controls whether or not the client will attempt to auto-migrate
|
||||
// v1 groups to v2 groups.
|
||||
// Controls whether or not the client will show the manual migration UI.
|
||||
// Will only offer migrations if all members can be migrated.
|
||||
//
|
||||
// TODO: Make this a remote config flag?
|
||||
// TODO: We might not need this flag.
|
||||
@objc
|
||||
public static let groupsV2MigrationAutoMigration = groupsV2MigrationMigration && false
|
||||
public static let groupsV2MigrationManualMigrationPolite = groupsV2Migration && build.includes(.dev)
|
||||
|
||||
// Controls whether or not the client will show the manual migration UI.
|
||||
// Will offer migrations even if some members cannot be migrated.
|
||||
//
|
||||
// TODO: Make this a remote config flag?
|
||||
// TODO: We might not need this flag.
|
||||
@objc
|
||||
public static let groupsV2MigrationManualMigrationAggressive = groupsV2Migration && false
|
||||
|
||||
// Controls whether or not the client will try to auto-migrate groups in the background.
|
||||
// Will only migrate groups if all members can be migrated.
|
||||
//
|
||||
// TODO: Make this a remote config flag?
|
||||
// TODO: We might not need this flag.
|
||||
@objc
|
||||
public static let groupsV2MigrationAutoMigrationPolite = groupsV2Migration && build.includes(.dev)
|
||||
|
||||
// Controls whether or not the client will try to auto-migrate groups in the background.
|
||||
// Will migrate groups even if some members cannot be migrated.
|
||||
//
|
||||
// TODO: Make this a remote config flag?
|
||||
// TODO: We might not need this flag.
|
||||
@objc
|
||||
public static let groupsV2MigrationAutoMigrationAggressive = groupsV2Migration && false
|
||||
|
||||
@objc
|
||||
public static let linkedPhones = build.includes(.internalPreview)
|
||||
|
||||
@objc
|
||||
public static let isUsingProductionService = true
|
||||
public static let isUsingProductionService = false
|
||||
|
||||
@objc
|
||||
public static let useOrphanDataCleaner = true
|
||||
|
||||
@ -49,13 +49,34 @@ public class RemoteConfig: BaseFlags {
|
||||
}
|
||||
|
||||
@objc
|
||||
public static var maxGroupsV2MemberCount: UInt {
|
||||
public static var groupsV2MaxGroupSizeRecommended: UInt {
|
||||
let defaultValue: UInt = 151
|
||||
guard AppReadiness.isAppReady else {
|
||||
owsFailDebug("Storage is not yet ready.")
|
||||
return defaultValue
|
||||
}
|
||||
guard let rawValue: AnyObject = value(.maxGroupsV2MemberCount) else {
|
||||
guard let rawValue: AnyObject = value(.groupsV2MaxGroupSizeRecommended) else {
|
||||
return defaultValue
|
||||
}
|
||||
guard let stringValue = rawValue as? String else {
|
||||
owsFailDebug("Unexpected value.")
|
||||
return defaultValue
|
||||
}
|
||||
guard let uintValue = UInt(stringValue) else {
|
||||
owsFailDebug("Invalid value.")
|
||||
return defaultValue
|
||||
}
|
||||
return uintValue
|
||||
}
|
||||
|
||||
@objc
|
||||
public static var groupsV2MaxGroupSizeHardLimit: UInt {
|
||||
let defaultValue: UInt = 1001
|
||||
guard AppReadiness.isAppReady else {
|
||||
owsFailDebug("Storage is not yet ready.")
|
||||
return defaultValue
|
||||
}
|
||||
guard let rawValue: AnyObject = value(.groupsV2MaxGroupSizeHardLimit) else {
|
||||
return defaultValue
|
||||
}
|
||||
guard let stringValue = rawValue as? String else {
|
||||
@ -257,7 +278,8 @@ private struct Flags {
|
||||
// Values defined in this array remain set once they are
|
||||
// set regardless of the remote state.
|
||||
enum StickyValuesFlags: String, FlagType {
|
||||
case maxGroupsV2MemberCount
|
||||
case groupsV2MaxGroupSizeRecommended
|
||||
case groupsV2MaxGroupSizeHardLimit
|
||||
}
|
||||
|
||||
// We filter the received config down to just the supported values.
|
||||
@ -265,7 +287,8 @@ private struct Flags {
|
||||
// set because we cached a value before it went public. e.g. if we set
|
||||
// a sticky value to X in beta then remove it before going to production.
|
||||
enum SupportedValuesFlags: String, FlagType {
|
||||
case maxGroupsV2MemberCount
|
||||
case groupsV2MaxGroupSizeRecommended
|
||||
case groupsV2MaxGroupSizeHardLimit
|
||||
case clientExpiration
|
||||
case researchMegaphone
|
||||
}
|
||||
@ -284,7 +307,8 @@ private protocol FlagType: CaseIterable {
|
||||
private extension FlagType {
|
||||
var rawFlag: String {
|
||||
switch rawValue {
|
||||
case "maxGroupsV2MemberCount": return "global.maxGroupSize"
|
||||
case "groupsV2MaxGroupSizeRecommended": return "global.groupsv2.maxGroupSize"
|
||||
case "groupsV2MaxGroupSizeHardLimit": return "global.groupsv2.groupSizeHardLimit"
|
||||
case "researchMegaphone": return "research.megaphone.1"
|
||||
default: return Flags.prefix + rawValue
|
||||
}
|
||||
|
||||
@ -95,8 +95,12 @@ public class GroupManager: NSObject {
|
||||
// GroupsV2 TODO: Finalize this value with the designers.
|
||||
public static let groupUpdateTimeoutDuration: TimeInterval = 30
|
||||
|
||||
public static var maxGroupsV2MemberCount: UInt {
|
||||
return RemoteConfig.maxGroupsV2MemberCount
|
||||
public static var groupsV2MaxGroupSizeRecommended: UInt {
|
||||
return RemoteConfig.groupsV2MaxGroupSizeRecommended
|
||||
}
|
||||
|
||||
public static var groupsV2MaxGroupSizeHardLimit: UInt {
|
||||
return RemoteConfig.groupsV2MaxGroupSizeHardLimit
|
||||
}
|
||||
|
||||
public static let maxGroupNameLength: Int = 32
|
||||
@ -1912,7 +1916,7 @@ public class GroupManager: NSObject {
|
||||
|
||||
let didDMConfigChange = oldDMConfiguration.asToken != newDMConfiguration.asToken
|
||||
let didGroupModelChange = !oldGroupModelV1.isEqual(to: newGroupModelV2,
|
||||
comparisonMode: .userFacingOnly)
|
||||
comparisonMode: .migration)
|
||||
if didDMConfigChange || didGroupModelChange {
|
||||
insertGroupUpdateInfoMessage(groupThread: groupThreadV2,
|
||||
oldGroupModel: oldGroupModelV1,
|
||||
|
||||
@ -21,7 +21,12 @@ typedef NS_CLOSED_ENUM(
|
||||
|
||||
typedef NS_CLOSED_ENUM(NSUInteger, TSGroupModelComparisonMode) {
|
||||
TSGroupModelComparisonMode_CompareAll,
|
||||
TSGroupModelComparisonMode_UserFacingOnly
|
||||
TSGroupModelComparisonMode_UserFacingOnly,
|
||||
// Migration mode only considers user-facing changes,
|
||||
// but ignores changes in roles. All v1 group members
|
||||
// become v2 admins, but we don't need to surface that
|
||||
// to users.
|
||||
TSGroupModelComparisonMode_Migration
|
||||
};
|
||||
|
||||
// NOTE: This class is tightly coupled to TSGroupModelBuilder.
|
||||
|
||||
@ -174,6 +174,7 @@ NSUInteger const TSGroupModelSchemaVersion = 1;
|
||||
}
|
||||
break;
|
||||
case TSGroupModelComparisonMode_UserFacingOnly:
|
||||
case TSGroupModelComparisonMode_Migration:
|
||||
break;
|
||||
}
|
||||
|
||||
@ -183,7 +184,8 @@ NSUInteger const TSGroupModelSchemaVersion = 1;
|
||||
if (![NSObject isNullableObject:self.groupAvatarData equalTo:other.groupAvatarData]) {
|
||||
return NO;
|
||||
}
|
||||
if (![NSObject isNullableObject:self.addedByAddress equalTo:other.addedByAddress]) {
|
||||
if (comparisonMode != TSGroupModelComparisonMode_Migration
|
||||
&& ![NSObject isNullableObject:self.addedByAddress equalTo:other.addedByAddress]) {
|
||||
return NO;
|
||||
}
|
||||
NSSet<SignalServiceAddress *> *myGroupMembersSet = [NSSet setWithArray:_groupMembers];
|
||||
|
||||
@ -102,7 +102,7 @@ public class TSGroupModelV2: TSGroupModel {
|
||||
switch comparisonMode {
|
||||
case .compareAll:
|
||||
return false
|
||||
case .userFacingOnly:
|
||||
case .userFacingOnly, .migration:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user