Merge branch 'charlesmchen/groupsV2migration2'

This commit is contained in:
Matthew Chen 2020-10-30 09:05:41 -03:00
commit e04de976bb
35 changed files with 1318 additions and 293 deletions

View File

@ -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 */,

View File

@ -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;

View File

@ -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];

View File

@ -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();

View File

@ -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)
})
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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]

View File

@ -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 couldnt 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 couldnt 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 couldnt 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 couldnt 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" = "Cant 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 cant 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 cant 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.";

View File

@ -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

View File

@ -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)
)
}
}

View File

@ -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

View File

@ -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;
}

View File

@ -556,7 +556,7 @@ public class GroupsV2ChangeSetImpl: NSObject, GroupsV2ChangeSet {
continue
}
guard allMemberCount <= GroupManager.maxGroupsV2MemberCount else {
guard allMemberCount <= GroupManager.groupsV2MaxGroupSizeHardLimit else {
throw GroupsV2Error.tooManyMembers
}

View File

@ -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
}
}

View File

@ -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.");

View File

@ -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

View File

@ -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();

View File

@ -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
*/

View File

@ -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) {

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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);

View File

@ -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 {

View File

@ -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.

View File

@ -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

View File

@ -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
}

View File

@ -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,

View File

@ -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.

View File

@ -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];

View File

@ -102,7 +102,7 @@ public class TSGroupModelV2: TSGroupModel {
switch comparisonMode {
case .compareAll:
return false
case .userFacingOnly:
case .userFacingOnly, .migration:
return true
}
}