diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index c9463b503c..b72917b0a7 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -282,7 +282,6 @@ 34BBC858220C7ADA00857249 /* ImageEditorContents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BBC853220C7ADA00857249 /* ImageEditorContents.swift */; }; 34BBC859220C7ADA00857249 /* ImageEditorStrokeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BBC854220C7ADA00857249 /* ImageEditorStrokeItem.swift */; }; 34BBC85A220C7ADA00857249 /* ImageEditorTextItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BBC855220C7ADA00857249 /* ImageEditorTextItem.swift */; }; - 34BBC85B220C7ADA00857249 /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BBC856220C7ADA00857249 /* OrderedDictionary.swift */; }; 34BBC85D220D19D600857249 /* ImageEditorPanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BBC85C220D19D600857249 /* ImageEditorPanGestureRecognizer.swift */; }; 34BBC861220E883300857249 /* ImageEditorModelTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BBC85F220E883200857249 /* ImageEditorModelTest.swift */; }; 34BBC862220E883300857249 /* ImageEditorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34BBC860220E883200857249 /* ImageEditorTest.swift */; }; @@ -1154,7 +1153,6 @@ 34BBC853220C7ADA00857249 /* ImageEditorContents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageEditorContents.swift; sourceTree = ""; }; 34BBC854220C7ADA00857249 /* ImageEditorStrokeItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageEditorStrokeItem.swift; sourceTree = ""; }; 34BBC855220C7ADA00857249 /* ImageEditorTextItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageEditorTextItem.swift; sourceTree = ""; }; - 34BBC856220C7ADA00857249 /* OrderedDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderedDictionary.swift; sourceTree = ""; }; 34BBC85C220D19D600857249 /* ImageEditorPanGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageEditorPanGestureRecognizer.swift; sourceTree = ""; }; 34BBC85F220E883200857249 /* ImageEditorModelTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageEditorModelTest.swift; sourceTree = ""; }; 34BBC860220E883200857249 /* ImageEditorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageEditorTest.swift; sourceTree = ""; }; @@ -2402,7 +2400,6 @@ 34BBC84A220B2CB200857249 /* ImageEditorTextViewController.swift */, 340872C022394CAA00CB25B0 /* ImageEditorTransform.swift */, 34BEDB1221C43F69007B0EAE /* ImageEditorView.swift */, - 34BBC856220C7ADA00857249 /* OrderedDictionary.swift */, 34080F03222858DC0087E99F /* OWSViewController+ImageEditor.swift */, 346B4A762369BFC600B56007 /* PermissiveGestureRecognizer.swift */, ); @@ -4272,7 +4269,6 @@ 346129AB1FD1F0EE00532771 /* OWSFormat.m in Sources */, 34AC0A12211B39EA00997B47 /* ContactTableViewCell.m in Sources */, 451F8A461FD715BA005CB9DA /* OWSGroupAvatarBuilder.m in Sources */, - 34BBC85B220C7ADA00857249 /* OrderedDictionary.swift in Sources */, 34C2EEB42270D1CE00BCA1D0 /* StickerView.swift in Sources */, 346129961FD1E30000532771 /* OWSDatabaseMigration.m in Sources */, 346129FB1FD5F31400532771 /* OWS101ExistingUsersBlockOnIdentityChange.m in Sources */, diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index ea3af86ad7..a79d8d89f0 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -893,7 +893,7 @@ /* table cell label in conversation settings */ "DISAPPEARING_MESSAGES" = "Disappearing Messages"; -/* Info Message when added to a group which has enabled disappearing messages. Embeds {{time amount}} before messages disappear, see the *_TIME_AMOUNT strings for context. */ +/* Info Message when added to a group which has enabled disappearing messages. Embeds {{time amount}} before messages disappear. See the *_TIME_AMOUNT strings for context. */ "DISAPPEARING_MESSAGES_CONFIGURATION_GROUP_EXISTING_FORMAT" = "Messages in this conversation will disappear after %@."; /* subheading in conversation settings */ @@ -1163,27 +1163,102 @@ /* Title of notification shown during GRDB migration indicating that user may need to open app to view their content. */ "GRDB_MIGRATION_NOTIFICATION_TITLE" = "Optimizing Database"; +/* Message indicating that the access to the group's attributes was changed by the local user. Embeds {{new access level}}. */ +"GROUP_ACCESS_ATTRIBUTES_UPDATED_BY_LOCAL_USER_FORMAT" = "You changed who can edit group info to “%@“."; + +/* Message indicating that the access to the group's attributes was changed by a remote user. Embeds {{ %1$@ user who changed the access, %2$@ new access level}}. */ +"GROUP_ACCESS_ATTRIBUTES_UPDATED_BY_REMOTE_USER_FORMAT" = "%@ changed who can edit group info to “%@“."; + +/* Message indicating that the access to the group's attributes was changed. Embeds {{new access level}}. */ +"GROUP_ACCESS_ATTRIBUTES_UPDATED_FORMAT" = "Group info can be changed by “%@“."; + +/* Description of the 'admins only' access level. */ +"GROUP_ACCESS_LEVEL_ADMINISTRATORS" = "Admins Only"; + +/* Description of the 'all users' access level. */ +"GROUP_ACCESS_LEVEL_ANY" = "Any User"; + +/* Description of the 'all members' access level. */ +"GROUP_ACCESS_LEVEL_MEMBER" = "All Members"; + +/* Description of the 'unknown' access level. */ +"GROUP_ACCESS_LEVEL_UNKNOWN" = "Unknown"; + +/* Message indicating that the access to the group's members was changed by the local user. Embeds {{new access level}}. */ +"GROUP_ACCESS_MEMBERS_UPDATED_BY_LOCAL_USER_FORMAT" = "You changed who can edit group membership to “%@“."; + +/* Message indicating that the access to the group's members was changed by a remote user. Embeds {{ %1$@ user who changed the access, %2$@ new access level}}. */ +"GROUP_ACCESS_MEMBERS_UPDATED_BY_REMOTE_USER_FORMAT" = "%@ changed who can edit group membership to “%@“."; + +/* Message indicating that the access to the group's members was changed. Embeds {{new access level}}. */ +"GROUP_ACCESS_MEMBERS_UPDATED_FORMAT" = "Group membership can be changed by “%@“."; + /* Error indicating that a member cannot be added to a group. */ "GROUP_CANNOT_ADD_INVALID_MEMBER" = "This user cannot be added to the group."; -/* conversation history entry */ -"GROUP_CREATED" = "Group created."; - -/* conversation history entry when the local user created a group */ +/* Message indicating that group was created by the local user. */ "GROUP_CREATED_BY_LOCAL_USER" = "You created the group."; -/* conversation history entry after a remote user added you to a group. Embeds {{remote user name}} */ +/* Message indicating that group was created by another user. Embeds {{remote user name}}. */ "GROUP_CREATED_BY_REMOTE_USER_FORMAT" = "%@ added you to the group."; +/* Message indicating that group was created by an unknown user. */ +"GROUP_CREATED_BY_UNKNOWN_USER" = "Group was created."; + /* Message shown in conversation view that indicates there were issues with group creation. */ "GROUP_CREATION_FAILED" = "Not all members could be added to the group. Tap to retry."; +/* Message indicating that the local user was added to the group by another user. Embeds {{remote user name}}. */ +"GROUP_LOCAL_USER_ADDED_TO_GROUP_BY_REMOTE_USER_FORMAT" = "%@ added you."; + +/* Message indicating that the local user was granted administrator role. */ +"GROUP_LOCAL_USER_GRANTED_ADMINISTRATOR" = "You are now an admin."; + +/* Message indicating that the local user was granted administrator role by another user. Embeds {{remote user name}}. */ +"GROUP_LOCAL_USER_GRANTED_ADMINISTRATOR_BY_REMOTE_USER_FORMAT" = "%@ made you an admin."; + +/* Message indicating that the local user accepted an invite to the group. */ +"GROUP_LOCAL_USER_INVITE_ACCEPTED" = "You accepted an invitation to the group."; + +/* Message indicating that the local user accepted an invite to the group. Embeds {{user who invited the local user}}. */ +"GROUP_LOCAL_USER_INVITE_ACCEPTED_FORMAT" = "You accepted an invitation to the group from $@."; + +/* Message indicating that the local user declined an invite to the group. */ +"GROUP_LOCAL_USER_INVITE_DECLINED_BY_LOCAL_USER" = "You declined an invitation to the group."; + +/* Message indicating that the local user declined an invite to the group. Embeds {{user who invited the local user}}. */ +"GROUP_LOCAL_USER_INVITE_DECLINED_FORMAT" = "You declined an invitation to the group from $@."; + +/* Message indicating that the local user's invite was revoked by another user. Embeds {{remote user name}}. */ +"GROUP_LOCAL_USER_INVITE_REVOKED_BY_REMOTE_USER_FORMAT" = "%@ revoked your invitation to the group."; + +/* Message indicating that the local user's invite was revoked by an unknown user. */ +"GROUP_LOCAL_USER_INVITE_REVOKED_BY_UNKNOWN_USER" = "Your invitation to the group was revoked."; + +/* Message indicating that the local user was invited to the group by another user. Embeds {{remote user name}}. */ +"GROUP_LOCAL_USER_INVITED_BY_REMOTE_USER_FORMAT" = "%@ invited you."; + +/* Message indicating that the local user was invited to the group. */ +"GROUP_LOCAL_USER_INVITED_TO_THE_GROUP" = "You were invited to the group."; + +/* Message indicating that the local user has joined the group. */ +"GROUP_LOCAL_USER_JOINED_THE_GROUP" = "You joined the group."; + +/* Message indicating that the local user was removed from the group by another user. Embeds {{remote user name}}. */ +"GROUP_LOCAL_USER_REMOVED_BY_REMOTE_USER_FORMAT" = "%@ removed you."; + +/* Message indicating that the local user was removed from the group by an unknown user. */ +"GROUP_LOCAL_USER_REMOVED_BY_UNKNOWN_USER" = "You were removed from the group."; + +/* Message indicating that the local user had their administrator role revoked. */ +"GROUP_LOCAL_USER_REVOKED_ADMINISTRATOR" = "Your admin privileges were revoked."; + +/* Message indicating that the local user had their administrator role revoked by another user. Embeds {{remote user name}}. */ +"GROUP_LOCAL_USER_REVOKED_ADMINISTRATOR_BY_REMOTE_USER_FORMAT" = "%@ revoked your admin privileges."; + /* Conversation settings table section title */ "GROUP_MANAGEMENT_SECTION" = "Group Management"; -/* No comment provided by engineer. */ -"GROUP_MEMBER_LEFT" = " %@ left the group. "; - /* Button label for the 'call group member' button */ "GROUP_MEMBERS_CALL" = "Call"; @@ -1202,37 +1277,139 @@ /* Button label for the 'send message to group member' button */ "GROUP_MEMBERS_SEND_MESSAGE" = "Message"; -/* conversation history entry */ +/* Message indicating that a remote user has accepted their invite. Embeds {{remote user name}}. */ +"GROUP_REMOTE_USER_ACCEPTED_INVITE_FORMAT" = "%@ accepted an invitation to the group."; + +/* Message indicating that a remote user has accepted an invite from the local user. Embeds {{remote user name}}. */ +"GROUP_REMOTE_USER_ACCEPTED_INVITE_FROM_LOCAL_USER_FORMAT" = "%@ accepted your invitation to the group."; + +/* Message indicating that a remote user has accepted their invite. Embeds {{ %1$@ user who accepted their invite, %2$@ user who invited the user}}. */ +"GROUP_REMOTE_USER_ACCEPTED_INVITE_FROM_REMOTE_USER_FORMAT" = "%1$@ accepted an invitation to the group from %2$@."; + +/* Message indicating that a remote user was added to the group by the local user. Embeds {{remote user name}}. */ +"GROUP_REMOTE_USER_ADDED_TO_GROUP_BY_LOCAL_USER_FORMAT" = "You added %@."; + +/* Message indicating that a remote user was added to the group by another user. Embeds {{ %1$@ user who added the user, %2$@ user who was added}}. */ +"GROUP_REMOTE_USER_ADDED_TO_GROUP_BY_REMOTE_USER_FORMAT" = "%1$@ added %2$@."; + +/* Message indicating that a remote user was added to the group. Embeds {{remote user name}}. */ +"GROUP_REMOTE_USER_ADDED_TO_GROUP_FORMAT" = "%@ was added to the group."; + +/* Message indicating that a remote user has declined their invite. */ +"GROUP_REMOTE_USER_DECLINED_INVITE" = "1 person declined the invitation to the group."; + +/* Message indicating that a remote user has declined their invite. Embeds {{ user who invited them }}. */ +"GROUP_REMOTE_USER_DECLINED_INVITE_FORMAT" = "1 person invited by %@ declined the invitation to the group."; + +/* Message indicating that a remote user has declined an invite to the group from the local user. Embeds {{remote user name}}. */ +"GROUP_REMOTE_USER_DECLINED_INVITE_FROM_LOCAL_USER_FORMAT" = "%@ declined your invitation to the group."; + +/* Message indicating that a remote user was granted administrator role. Embeds {{remote user name}}. */ +"GROUP_REMOTE_USER_GRANTED_ADMINISTRATOR" = "%@ is now an admin"; + +/* Message indicating that a remote user was granted administrator role by local user. Embeds {{remote user name}}. */ +"GROUP_REMOTE_USER_GRANTED_ADMINISTRATOR_BY_LOCAL_USER" = "You made %@ an admin."; + +/* Message indicating that a remote user was granted administrator role by another user. Embeds {{ %1$@ user who granted, %2$@ user who was granted administrator role}}. */ +"GROUP_REMOTE_USER_GRANTED_ADMINISTRATOR_BY_REMOTE_USER_FORMAT" = "%1$@ made %2$@ an admin."; + +/* Message indicating that a single remote user's invite was revoked. */ +"GROUP_REMOTE_USER_INVITE_REVOKED_1" = "Invitation to the group was revoked for 1 person."; + +/* Message indicating that a remote user's invite was revoked by the local user. Embeds {{remote user name}}. */ +"GROUP_REMOTE_USER_INVITE_REVOKED_BY_LOCAL_USER_FORMAT" = "Invitation to the group was revoked for %@."; + +/* Message indicating that a single remote user's invite was revoked by a remote user. Embeds {{ user who revoked the invite }}. */ +"GROUP_REMOTE_USER_INVITE_REVOKED_BY_REMOTE_USER_1_FORMAT" = "%@ revoked an invitation to the group for 1 person."; + +/* Message indicating that a group of remote users' invites were revoked by a remote user. Embeds {{ %1$@ user who revoked the invite, %2$@ number of users }}. */ +"GROUP_REMOTE_USER_INVITE_REVOKED_BY_REMOTE_USER_N_FORMAT" = "%1$@ revoked an invitation to the group for %2$@ people."; + +/* Message indicating that a group of remote users' invites were revoked. Embeds {{ number of users }}. */ +"GROUP_REMOTE_USER_INVITE_REVOKED_N_FORMAT" = "Invitations to the group were revoked for %@ people."; + +/* Message indicating that a single remote user was invited to the group. */ +"GROUP_REMOTE_USER_INVITED_1" = "1 person was invited to the group."; + +/* Message indicating that a remote user was invited to the group by the local user. Embeds {{remote user name}}. */ +"GROUP_REMOTE_USER_INVITED_BY_LOCAL_USER_FORMAT" = "You invited %@ to the group."; + +/* Message indicating that a single remote user was invited to the group by the local user. Embeds {{ user who invited the user }}. */ +"GROUP_REMOTE_USER_INVITED_BY_REMOTE_USER_1_FORMAT" = "%@ invited 1 person to the group."; + +/* Message indicating that a group of remote users were invited to the group by the local user. Embeds {{ %1$@ user who invited the user, %2$@ number of invited users }}. */ +"GROUP_REMOTE_USER_INVITED_BY_REMOTE_USER_N_FORMAT" = "%1$@ invited %2$@ people to the group."; + +/* Message indicating that a group of remote users were invited to the group. Embeds {{number of invited users}}. */ +"GROUP_REMOTE_USER_INVITED_N_FORMAT" = "%@ people were invited to the group."; + +/* Message indicating that a remote user was added to the group. Embeds {{remote user name}}. */ +"GROUP_REMOTE_USER_JOINED_GROUP_FORMAT" = "%@ joined the group."; + +/* Message indicating that a remote user has left the group. Embeds {{remote user name}}. */ +"GROUP_REMOTE_USER_LEFT_GROUP_FORMAT" = "%@ left the group."; + +/* Message indicating that a remote user was removed from the group by the local user. Embeds {{remote user name}}. */ +"GROUP_REMOTE_USER_REMOVED_BY_LOCAL_USER_FORMAT" = "You removed %@."; + +/* Message indicating that the remote user was removed from the group. Embeds {{ %1$@ user who removed the user, %2$@ user who was removed}}. */ +"GROUP_REMOTE_USER_REMOVED_FROM_GROUP_BY_REMOTE_USER_FORMAT" = "%1$@ removed %2$@."; + +/* Message indicating that a remote user had their administrator role revoked. Embeds {{remote user name}}. */ +"GROUP_REMOTE_USER_REVOKED_ADMINISTRATOR" = "%@ had their admin privileges revoked."; + +/* Message indicating that a remote user had their administrator role revoked by local user. Embeds {{remote user name}}. */ +"GROUP_REMOTE_USER_REVOKED_ADMINISTRATOR_BY_LOCAL_USER" = "You revoked admin privileges from %@."; + +/* Message indicating that a remote user had their administrator role revoked by another user. Embeds {{ %1$@ user who revoked, %2$@ user who was granted administrator role}}. */ +"GROUP_REMOTE_USER_REVOKED_ADMINISTRATOR_BY_REMOTE_USER_FORMAT" = "%1$@ revoked admin privileges from %2$@."; + +/* Info message indicating that the group was updated by an unknown user. */ "GROUP_UPDATED" = "Group updated."; -/* conversation history entry. embeds {0: all but the final member} and {1: the final member} */ -"GROUP_UPDATED_ADDED_MULTIPLE_MEMBERS_FORMAT" = "%@ and %@ joined the group."; +/* Message indicating that the group's avatar was removed. */ +"GROUP_UPDATED_AVATAR_REMOVED" = "The group avatar was removed."; -/* conversation history entry. Embeds the {{added user's name}} */ -"GROUP_UPDATED_ADDED_ONE_MEMBER_FORMAT" = "%@ joined the group."; +/* Message indicating that the group's avatar was removed. */ +"GROUP_UPDATED_AVATAR_REMOVED_BY_LOCAL_USER" = "You removed the avatar."; -/* conversation history entry */ -"GROUP_UPDATED_AVATAR_REMOVED" = "Removed the group avatar."; +/* Message indicating that the group's avatar was removed by a remote user. Embeds {{user who removed the avatar}}. */ +"GROUP_UPDATED_AVATAR_REMOVED_BY_REMOTE_USER_FORMAT" = "%@ removed the avatar."; -/* conversation history entry */ +/* Message indicating that the group's avatar was changed. */ "GROUP_UPDATED_AVATAR_UPDATED" = "Updated the group avatar."; -/* conversation history entry. */ +/* Message indicating that the group's avatar was changed. */ +"GROUP_UPDATED_AVATAR_UPDATED_BY_LOCAL_USER" = "You updated the avatar."; + +/* Message indicating that the group's avatar was changed by a remote user. Embeds {{user who changed the avatar}}. */ +"GROUP_UPDATED_AVATAR_UPDATED_BY_REMOTE_USER_FORMAT" = "%@ updated the avatar."; + +/* Info message indicating that the group was updated by the local user. */ "GROUP_UPDATED_BY_LOCAL_USER" = "You updated the group."; -/* conversation history entry. Embeds {{remote user's name}} */ +/* Info message indicating that the group was updated by another user. Embeds {{remote user name}}. */ "GROUP_UPDATED_BY_REMOTE_USER_FORMAT" = "%@ updated the group."; -/* conversation history entry */ -"GROUP_UPDATED_NAME_REMOVED" = "Removed the name."; +/* Message indicating that the group's name was removed. */ +"GROUP_UPDATED_NAME_REMOVED" = "The group name was removed."; -/* conversation history entry. Embeds {{group name}} */ +/* Message indicating that the group's name was removed by the local user. */ +"GROUP_UPDATED_NAME_REMOVED_BY_LOCAL_USER" = "You removed the group name."; + +/* Message indicating that the group's name was removed by a remote user. Embeds {{user who removed the name}}. */ +"GROUP_UPDATED_NAME_REMOVED_BY_REMOTE_USER_FORMAT" = "%@ removed the group name."; + +/* Message indicating that the group's name was changed by the local user. Embeds {{new group name}}. */ +"GROUP_UPDATED_NAME_UPDATED_BY_LOCAL_USER_FORMAT" = "You changed the group name to “%@“."; + +/* Message indicating that the group's name was changed by a remote user. Embeds {{ %1$@ user who changed the name, %2$@ new group name}}. */ +"GROUP_UPDATED_NAME_UPDATED_BY_REMOTE_USER_FORMAT" = "%1$@ changed the group name to “%2$@“."; + +/* Message indicating that the group's name was changed. Embeds {{new group name}}. */ "GROUP_UPDATED_NAME_UPDATED_FORMAT" = "Group name is now “%@”."; -/* conversation history entry */ -"GROUP_UPDATED_READDED_YOU" = "Re-added you to the group."; - -/* No comment provided by engineer. */ +/* Message indicating that the local user left the group. */ "GROUP_YOU_LEFT" = "You have left the group."; /* Error indicating that an error occurred while accepting an invite. */ @@ -1979,10 +2156,10 @@ /* Button text which opens the settings app */ "OPEN_SETTINGS_BUTTON" = "Settings"; -/* Info Message when {{other user}} disables or doesn't support disappearing messages */ +/* Info Message when another user disabled disappearing messages. Embeds {{name of other user}}. */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ disabled disappearing messages."; -/* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ +/* Info Message when another user enabled disappearing messages. Embeds {{name of other user}} and {{time amount}} before messages disappear. See the *_TIME_AMOUNT strings for context. */ "OTHER_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ set disappearing message time to %@."; /* Label warning the user that the Signal service may be down. */ @@ -2171,6 +2348,9 @@ /* Body for PIN megaphone when user already has a PIN */ "PINS_MEGAPHONE_HAS_PIN_BODY" = "Your Registration Lock is now called a PIN. Updating it takes seconds."; +/* Toast indication that the user will be reminded later to update their PIN. Embeds {{time until mandatory}} */ +"PINS_MEGAPHONE_HAS_PIN_SNOOZE_TOAST_FORMAT" = "PINS_MEGAPHONE_HAS_PIN_SNOOZE_TOAST_FORMAT"; + /* Title for PIN megaphone when user already has a PIN */ "PINS_MEGAPHONE_HAS_PIN_TITLE" = "Introducing PINs"; @@ -2180,6 +2360,9 @@ /* Body for PIN megaphone when user doesn't have a PIN */ "PINS_MEGAPHONE_NO_PIN_BODY" = "PINs add another layer of security to your Signal account."; +/* Toast indication that the user will be reminded later to setup their PIN. Embeds {{time until mandatory}} */ +"PINS_MEGAPHONE_NO_PIN_SNOOZE_TOAST_FORMAT" = "PINS_MEGAPHONE_NO_PIN_SNOOZE_TOAST_FORMAT"; + /* Title for PIN megaphone when user doesn't have a PIN */ "PINS_MEGAPHONE_NO_PIN_TITLE" = "Create a PIN"; @@ -3170,6 +3353,12 @@ /* Info message recorded in conversation history when local user has received an unknown message and has upgraded. */ "UNKNOWN_PROTOCOL_VERSION_UPGRADE_COMPLETE_WITHOUT_NAME" = "You can ask the sender to re-send this message now that you are using an up-to-date version of Signal."; +/* Info Message when an unknown user disabled disappearing messages. */ +"UNKNOWN_USER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "Disappearing messages were disabled."; + +/* Info Message when an unknown user enabled disappearing messages. Embeds {{time amount}} before messages disappear. see the *_TIME_AMOUNT strings for context. */ +"UNKNOWN_USER_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION" = "Disappearing message time was set to %@."; + /* Indicates an unknown or unrecognizable value. */ "UNKNOWN_VALUE" = "Unknown"; @@ -3329,11 +3518,11 @@ /* Activity indicator title, shown upon returning to the device manager, until you complete the provisioning process on desktop */ "WAITING_TO_COMPLETE_DEVICE_LINK_TEXT" = "Complete setup on Signal Desktop."; -/* Info Message when you disable disappearing messages */ +/* Info Message when you disabled disappearing messages. */ "YOU_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "You disabled disappearing messages."; /* alert body shown when trying to use features in the app before completing registration-related setup. */ "YOU_MUST_COMPLETE_ONBOARDING_BEFORE_PROCEEDING" = "You must complete setup before proceeding."; -/* Info message embedding a {{time amount}}, see the *_TIME_AMOUNT strings for context. */ +/* Info Message when you disabled disappearing messages. Embeds a {{time amount}} before messages disappear. see the *_TIME_AMOUNT strings for context. */ "YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION" = "You set disappearing message time to %@."; diff --git a/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.swift b/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.swift index f5ba76b4dc..1f169df3de 100644 --- a/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.swift +++ b/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.swift @@ -11,6 +11,7 @@ public class DisappearingMessageToken: MTLModel { @objc public var durationSeconds: UInt32 = 0 + @objc public init(isEnabled: Bool, durationSeconds: UInt32) { // Consider disabled if duration is zero. self.isEnabled = isEnabled && durationSeconds > 0 diff --git a/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.m b/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.m index 16a89f2666..7602a6f123 100644 --- a/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.m +++ b/SignalServiceKit/src/Messages/Interactions/OWSDisappearingConfigurationUpdateInfoMessage.m @@ -5,6 +5,7 @@ #import "OWSDisappearingConfigurationUpdateInfoMessage.h" #import "OWSDisappearingMessagesConfiguration.h" #import +#import NS_ASSUME_NONNULL_BEGIN @@ -134,43 +135,12 @@ NS_ASSUME_NONNULL_BEGIN - (NSString *)previewTextWithTransaction:(SDSAnyReadTransaction *)transaction { - if (self.createdInExistingGroup) { - OWSAssertDebug(self.configurationIsEnabled && self.configurationDurationSeconds > 0); - NSString *infoFormat = NSLocalizedString(@"DISAPPEARING_MESSAGES_CONFIGURATION_GROUP_EXISTING_FORMAT", - @"Info Message when added to a group which has enabled disappearing messages. Embeds {{time amount}} " - @"before messages disappear, see the *_TIME_AMOUNT strings for context."); - - NSString *durationString = [NSString formatDurationSeconds:self.configurationDurationSeconds useShortFormat:NO]; - return [NSString stringWithFormat:infoFormat, durationString]; - } else if (self.createdByRemoteName) { - if (self.configurationIsEnabled && self.configurationDurationSeconds > 0) { - NSString *infoFormat = NSLocalizedString(@"OTHER_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION", - @"Info Message when {{other user}} updates message expiration to {{time amount}}, see the " - @"*_TIME_AMOUNT " - @"strings for context."); - - NSString *durationString = - [NSString formatDurationSeconds:self.configurationDurationSeconds useShortFormat:NO]; - return [NSString stringWithFormat:infoFormat, self.createdByRemoteName, durationString]; - } else { - NSString *infoFormat = NSLocalizedString(@"OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION", - @"Info Message when {{other user}} disables or doesn't support disappearing messages"); - return [NSString stringWithFormat:infoFormat, self.createdByRemoteName]; - } - } else { - // Changed by localNumber on this device or via synced transcript - if (self.configurationIsEnabled && self.configurationDurationSeconds > 0) { - NSString *infoFormat = NSLocalizedString(@"YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION", - @"Info message embedding a {{time amount}}, see the *_TIME_AMOUNT strings for context."); - - NSString *durationString = - [NSString formatDurationSeconds:self.configurationDurationSeconds useShortFormat:NO]; - return [NSString stringWithFormat:infoFormat, durationString]; - } else { - return NSLocalizedString(@"YOU_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION", - @"Info Message when you disable disappearing messages"); - } - } + DisappearingMessageToken *newToken = + [[DisappearingMessageToken alloc] initWithIsEnabled:self.configurationIsEnabled + durationSeconds:self.configurationDurationSeconds]; + return [TSInfoMessage legacyDisappearingMessageUpdateDescriptionWithToken:newToken + wasAddedToExistingGroup:self.createdInExistingGroup + updaterName:self.createdByRemoteName]; } @end diff --git a/SignalServiceKit/src/Messages/Interactions/TSInfoMessage+Swift.swift b/SignalServiceKit/src/Messages/Interactions/TSInfoMessage+Swift.swift index 1610c7266b..df092c281a 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSInfoMessage+Swift.swift +++ b/SignalServiceKit/src/Messages/Interactions/TSInfoMessage+Swift.swift @@ -9,10 +9,16 @@ public extension TSInfoMessage { // MARK: - Dependencies - var contactsManager: ContactsManagerProtocol { + private var contactsManager: ContactsManagerProtocol { return SSKEnvironment.shared.contactsManager } + private var tsAccountManager: TSAccountManager { + return TSAccountManager.sharedInstance() + } + + // MARK: - + func groupUpdateDescription(transaction: SDSAnyReadTransaction) -> String { // for legacy group updates we persisted a pre-rendered string, rather than the details // to generate that string @@ -20,176 +26,1256 @@ public extension TSInfoMessage { return customMessage } - let genericDescription = NSLocalizedString("GROUP_UPDATED", comment: "conversation history entry") - guard let newGroupModel = self.newGroupModel else { - return genericDescription + // Legacy info message before we began embedding user info. + return GroupUpdate.defaultGroupUpdateDescription(groupUpdateSourceAddress: groupUpdateSourceAddress, + transaction: transaction) } - guard let groupUpdater = self.groupUpdateSourceAddress else { - if self.previousGroupModel == nil { - switch newGroupModel.groupsVersion { - case .V1: - return NSLocalizedString("GROUP_CREATED", comment: "conversation history entry") - case .V2: - return genericDescription - } - } else { - return genericDescription - } - } - - return groupUpdateDescription(fromGroupState: self.previousGroupModel, - toGroupState: newGroupModel, - groupUpdater: groupUpdater, + return groupUpdateDescription(oldGroupModel: self.previousGroupModel, + newGroupModel: newGroupModel, transaction: transaction) } +} - // GroupsV2 TODO: Update this to reflect the v2 group properties. - func groupUpdateDescription(fromGroupState: TSGroupModel?, - toGroupState: TSGroupModel, - groupUpdater: SignalServiceAddress, - transaction: SDSAnyReadTransaction) -> String { +// MARK: - - guard let localAddress = TSAccountManager.sharedInstance().localAddress else { +extension TSInfoMessage { + private func groupUpdateDescription(oldGroupModel: TSGroupModel?, + newGroupModel: TSGroupModel, + transaction: SDSAnyReadTransaction) -> String { + + guard let localAddress = tsAccountManager.localAddress else { owsFailDebug("missing local address") - return NSLocalizedString("GROUP_UPDATED", comment: "conversation history entry") + return GroupUpdate.defaultGroupUpdateDescription(groupUpdateSourceAddress: groupUpdateSourceAddress, + transaction: transaction) } - let updaterName = self.contactsManager.displayName(for: groupUpdater, transaction: transaction) + let groupUpdate = GroupUpdate(newGroupModel: newGroupModel, + oldGroupModel: oldGroupModel, + oldDisappearingMessageToken: oldDisappearingMessageToken, + newDisappearingMessageToken: newDisappearingMessageToken, + localAddress: localAddress, + groupUpdateSourceAddress: groupUpdateSourceAddress, + transaction: transaction) + return groupUpdate.updateDescription + } - guard let fromGroupState = fromGroupState else { - // New Group was created - if groupUpdater.isLocalAddress { - return NSLocalizedString("GROUP_CREATED_BY_LOCAL_USER", - comment: "conversation history entry when the local user created a group") + @objc + public static func legacyDisappearingMessageUpdateDescription(token newToken: DisappearingMessageToken, + wasAddedToExistingGroup: Bool, + updaterName: String?) -> String { + + // This might be zero if DMs are not enabled. + let durationString = NSString.formatDurationSeconds(newToken.durationSeconds, useShortFormat: false) + + if wasAddedToExistingGroup { + assert(newToken.isEnabled) + let format = NSLocalizedString("DISAPPEARING_MESSAGES_CONFIGURATION_GROUP_EXISTING_FORMAT", + comment: "Info Message when added to a group which has enabled disappearing messages. Embeds {{time amount}} before messages disappear. See the *_TIME_AMOUNT strings for context.") + return String(format: format, durationString) + } else if let updaterName = updaterName { + if newToken.isEnabled { + let format = NSLocalizedString("OTHER_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION", + comment: "Info Message when another user enabled disappearing messages. Embeds {{name of other user}} and {{time amount}} before messages disappear. See the *_TIME_AMOUNT strings for context.") + return String(format: format, updaterName, durationString) } else { - let format = NSLocalizedString("GROUP_CREATED_BY_REMOTE_USER_FORMAT", - comment: "conversation history entry after a remote user added you to a group. Embeds {{remote user name}}") + let format = NSLocalizedString("OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION", + comment: "Info Message when another user disabled disappearing messages. Embeds {{name of other user}}.") return String(format: format, updaterName) } - } - - // GroupsV2 TODO: Revisit this for v2 groups. - if localAddress == groupUpdater, - fromGroupState.groupMembership.allUsers.contains(localAddress), - !toGroupState.groupMembership.allUsers.contains(localAddress) { - return NSLocalizedString("GROUP_YOU_LEFT", comment: "") - } - if fromGroupState.groupMembership.allUsers.contains(groupUpdater), - !toGroupState.groupMembership.allUsers.contains(groupUpdater) { - let format = NSLocalizedString("GROUP_MEMBER_LEFT", comment: "") - return String(format: format, updaterName) - } - - // Existing Group was updated - - var lines: [String] = [] - if groupUpdater.isLocalAddress { - lines.append(NSLocalizedString("GROUP_UPDATED_BY_LOCAL_USER", - comment: "conversation history entry.")) } else { - let format = NSLocalizedString("GROUP_UPDATED_BY_REMOTE_USER_FORMAT", - comment: "conversation history entry. Embeds {{remote user's name}}") - let text = String(format: format, updaterName) - lines.append(text) - } - - if toGroupState.groupName != fromGroupState.groupName { - if let toGroupName = toGroupState.groupName, toGroupName.count > 0 { - let format = NSLocalizedString("GROUP_UPDATED_NAME_UPDATED_FORMAT", - comment: "conversation history entry. Embeds {{group name}}") - let text = String(format: format, toGroupName) - lines.append(text) + // Changed by localNumber on this device or via synced transcript + if newToken.isEnabled { + let format = NSLocalizedString("YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION", + comment: "Info Message when you disabled disappearing messages. Embeds a {{time amount}} before messages disappear. see the *_TIME_AMOUNT strings for context.") + return String(format: format, durationString) } else { - let text = NSLocalizedString("GROUP_UPDATED_NAME_REMOVED", - comment: "conversation history entry") - lines.append(text) + return NSLocalizedString("YOU_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION", + comment: "Info Message when you disabled disappearing messages.") } } + } +} - if fromGroupState.groupAvatarData != toGroupState.groupAvatarData { - if let toGroupAvatarData = toGroupState.groupAvatarData, toGroupAvatarData.count > 0 { - let text = NSLocalizedString("GROUP_UPDATED_AVATAR_UPDATED", - comment: "conversation history entry") - lines.append(text) - } else { - let text = NSLocalizedString("GROUP_UPDATED_AVATAR_REMOVED", - comment: "conversation history entry") - lines.append(text) - } +// MARK: - + +extension TSInfoMessage { + + private func infoMessageValue(forKey key: InfoMessageUserInfoKey) -> T? { + guard let infoMessageUserInfo = self.infoMessageUserInfo else { + return nil } - let addedMembers = toGroupState.groupMembers.filter { !fromGroupState.groupMembers.contains($0) } - let localUserAddedToGroup = (addedMembers.first { $0 == TSAccountManager.localAddress } != nil) - if localUserAddedToGroup { - let text = NSLocalizedString("GROUP_UPDATED_READDED_YOU", - comment: "conversation history entry") - lines.append(text) - } else if addedMembers.count == 1 { - let format = NSLocalizedString("GROUP_UPDATED_ADDED_ONE_MEMBER_FORMAT", - comment: "conversation history entry. Embeds the {{added user's name}}") + guard let groupModel = infoMessageUserInfo[key] as? T else { + assert(infoMessageUserInfo[key] == nil) + return nil + } - let userName = contactsManager.displayName(for: addedMembers[0], transaction: transaction) - let text = String(format: format, userName) - lines.append(text) - } else if addedMembers.count >= 2 { - let firstMembers = addedMembers[0..() + + fileprivate init(newGroupModel: TSGroupModel, + oldGroupModel: TSGroupModel?, + oldDisappearingMessageToken: DisappearingMessageToken?, + newDisappearingMessageToken: DisappearingMessageToken?, + localAddress: SignalServiceAddress, + groupUpdateSourceAddress: SignalServiceAddress?, + transaction: SDSAnyReadTransaction) { + self.newGroupModel = newGroupModel + self.localAddress = localAddress + self.groupUpdateSourceAddress = groupUpdateSourceAddress + self.transaction = transaction + self.newGroupMembership = newGroupModel.groupMembership + self.updater = GroupUpdate.updater(groupUpdateSourceAddress: groupUpdateSourceAddress, + transaction: transaction) + + switch updater { + case .unknown: + if newGroupModel.groupsVersion == .V2 { + owsFailDebug("Missing inviter info.") + } + default: + break + } + + populate(oldGroupModel: oldGroupModel, + oldDisappearingMessageToken: oldDisappearingMessageToken, + newDisappearingMessageToken: newDisappearingMessageToken) + } + + // MARK: - + + func populate(oldGroupModel: TSGroupModel?, + oldDisappearingMessageToken: DisappearingMessageToken?, + newDisappearingMessageToken: DisappearingMessageToken?) { + + if let oldGroupModel = oldGroupModel { + let oldGroupMembership = oldGroupModel.groupMembership + addMembershipUpdates(oldGroupMembership: oldGroupMembership) + + addAttributesUpdates(oldGroupModel: oldGroupModel) + + addAccessUpdates(oldGroupModel: oldGroupModel) + + addDisappearingMessageUpdates(oldToken: oldDisappearingMessageToken, + newToken: newDisappearingMessageToken) + } else { + // We're just learning of the group. + addGroupWasUpserted() + + // Skip description of overall group state (current name, avatar, members, etc.). + // + // Include a description of current DM state, if necessary. + addDisappearingMessageUpdates(oldToken: oldDisappearingMessageToken, + newToken: newDisappearingMessageToken) + } + + if itemMap.count < 1 { + if newGroupModel.groupsVersion == .V2 { + owsFailDebug("Group update without any items.") + } + addItem(.generic, copy: defaultGroupUpdateDescription) + } + } + + // MARK: - + + func addItem(_ type: UpdateType, + address: SignalServiceAddress? = nil, + copy: String) { + let item = UpdateItem(type: type, + address: address) + assert(itemMap.value(forKey: item) == nil) + itemMap.append(key: item, value: copy) + } + + func addItem(_ type: UpdateType, + address: SignalServiceAddress? = nil, + format: String, _ formatArgs: CVarArg...) { + let copy = String(format: format, arguments: formatArgs) + addItem(type, address: address, copy: copy) + } + + var updateDescription: String { + var lines = [String]() + + for (_, copy) in itemMap { + lines.append(copy) } return lines.joined(separator: "\n") } - // MARK: - Helpers - - private var previousGroupModel: TSGroupModel? { - guard let infoMessageUserInfo = self.infoMessageUserInfo else { - return nil + func wasUpdaterSameAs(_ address: SignalServiceAddress) -> Bool { + switch updater { + case .localUser: + return false + case .otherUser(_, let updaterAddress): + return updaterAddress == address + case .unknown: + return false } - - guard let groupModel = infoMessageUserInfo[.oldGroupModel] as? TSGroupModel else { - assert(infoMessageUserInfo[.oldGroupModel] == nil) - return nil - } - - return groupModel } - private var newGroupModel: TSGroupModel? { - guard let infoMessageUserInfo = self.infoMessageUserInfo else { - return nil - } - - guard let groupModel = infoMessageUserInfo[.newGroupModel] as? TSGroupModel else { - assert(infoMessageUserInfo[.newGroupModel] == nil) - return nil - } - - return groupModel - } - - private var groupUpdateSourceAddress: SignalServiceAddress? { - guard let infoMessageUserInfo = self.infoMessageUserInfo else { - return nil - } - - guard let address = infoMessageUserInfo[.groupUpdateSourceAddress] as? SignalServiceAddress else { - assert(infoMessageUserInfo[.groupUpdateSourceAddress] == nil) - return nil - } - - return address + func wasUpdaterLocalUser() -> Bool { + return wasUpdaterSameAs(localAddress) + } +} + +// MARK: - + +// When deciding which copy to render and how to format it, we usually take into account 3-5 pieces of information: +// +// * The old state +// * The new state +// * Who made the update (the updater) +// * Which user was affected (often account) +// * Whether or not the updater or account is the local user. +// * Some other info like "who did the inviting". +// +// e.g. if Alice sees that Bob declined his invite (invited by Carol) to a group... +// +// * The old state was "invited to the group". +// * The new state is "in the group". +// +// ...We infer that Bob declined an invite OR had their invite revoked. +// +// * "Bob" made the update. +// * "Bob" was affected. +// +// ...We infer that Bob declined the invite; it wasn't revoked by another user. +// +// * Neither the updater or account is the local user. +// +// ...So we don't want to special-case and say something like "You declined..." or "Your invite was revoked...", etc. +// +// * Carol "did the inviting". +// +// ...So the final copy should be something like "Bob declined his invitation from Carol." +extension GroupUpdate { + + // MARK: - Attributes + + func addAttributesUpdates(oldGroupModel: TSGroupModel) { + + let groupName = { (groupModel: TSGroupModel) -> String? in + if let name = groupModel.groupName?.stripped, name.count > 0 { + return name + } + return nil + } + let oldGroupName = groupName(oldGroupModel) + let newGroupName = groupName(newGroupModel) + if oldGroupName != newGroupName { + if let name = newGroupName { + switch updater { + case .localUser: + let format = NSLocalizedString("GROUP_UPDATED_NAME_UPDATED_BY_LOCAL_USER_FORMAT", + comment: "Message indicating that the group's name was changed by the local user. Embeds {{new group name}}.") + addItem(.groupName, format: format, name) + case .otherUser(let updaterName, _): + let format = NSLocalizedString("GROUP_UPDATED_NAME_UPDATED_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that the group's name was changed by a remote user. Embeds {{ %1$@ user who changed the name, %2$@ new group name}}.") + addItem(.groupName, format: format, updaterName, name) + case .unknown: + let format = NSLocalizedString("GROUP_UPDATED_NAME_UPDATED_FORMAT", + comment: "Message indicating that the group's name was changed. Embeds {{new group name}}.") + addItem(.groupName, format: format, name) + } + } else { + switch updater { + case .localUser: + addItem(.groupName, copy: NSLocalizedString("GROUP_UPDATED_NAME_REMOVED_BY_LOCAL_USER", + comment: "Message indicating that the group's name was removed by the local user.")) + case .otherUser(let updaterName, _): + let format = NSLocalizedString("GROUP_UPDATED_NAME_REMOVED_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that the group's name was removed by a remote user. Embeds {{user who removed the name}}.") + addItem(.groupName, format: format, updaterName) + case .unknown: + addItem(.groupName, copy: NSLocalizedString("GROUP_UPDATED_NAME_REMOVED", + comment: "Message indicating that the group's name was removed.")) + } + } + } + + if oldGroupModel.groupAvatarData != newGroupModel.groupAvatarData { + if let toGroupAvatarData = newGroupModel.groupAvatarData, toGroupAvatarData.count > 0 { + switch updater { + case .localUser: + addItem(.groupAvatar, copy: NSLocalizedString("GROUP_UPDATED_AVATAR_UPDATED_BY_LOCAL_USER", + comment: "Message indicating that the group's avatar was changed.")) + case .otherUser(let updaterName, _): + let format = NSLocalizedString("GROUP_UPDATED_AVATAR_UPDATED_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that the group's avatar was changed by a remote user. Embeds {{user who changed the avatar}}.") + addItem(.groupAvatar, format: format, updaterName) + case .unknown: + addItem(.groupAvatar, copy: NSLocalizedString("GROUP_UPDATED_AVATAR_UPDATED", + comment: "Message indicating that the group's avatar was changed.")) + } + } else { + switch updater { + case .localUser: + addItem(.groupAvatar, copy: NSLocalizedString("GROUP_UPDATED_AVATAR_REMOVED_BY_LOCAL_USER", + comment: "Message indicating that the group's avatar was removed.")) + case .otherUser(let updaterName, _): + let format = NSLocalizedString("GROUP_UPDATED_AVATAR_REMOVED_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that the group's avatar was removed by a remote user. Embeds {{user who removed the avatar}}.") + addItem(.groupAvatar, format: format, updaterName) + case .unknown: + addItem(.groupAvatar, copy: NSLocalizedString("GROUP_UPDATED_AVATAR_REMOVED", + comment: "Message indicating that the group's avatar was removed.")) + } + } + } + } + + // MARK: - Access + + func description(for access: GroupV2Access) -> String { + switch access { + case .unknown: + owsFailDebug("Unknown access level.") + return NSLocalizedString("GROUP_ACCESS_LEVEL_UNKNOWN", + comment: "Description of the 'unknown' access level.") + case .any: + return NSLocalizedString("GROUP_ACCESS_LEVEL_ANY", + comment: "Description of the 'all users' access level.") + case .member: + return NSLocalizedString("GROUP_ACCESS_LEVEL_MEMBER", + comment: "Description of the 'all members' access level.") + case .administrator: + return NSLocalizedString("GROUP_ACCESS_LEVEL_ADMINISTRATORS", + comment: "Description of the 'admins only' access level.") + } + } + + func addAccessUpdates(oldGroupModel: TSGroupModel) { + + let oldAccess = oldGroupModel.groupAccess + let newAccess = newGroupModel.groupAccess + + if oldAccess.members != newAccess.members { + let accessName = description(for: newAccess.members) + + switch updater { + case .localUser: + let format = NSLocalizedString("GROUP_ACCESS_MEMBERS_UPDATED_BY_LOCAL_USER_FORMAT", + comment: "Message indicating that the access to the group's members was changed by the local user. Embeds {{new access level}}.") + addItem(.accessMembers, format: format, accessName) + case .otherUser(let updaterName, _): + let format = NSLocalizedString("GROUP_ACCESS_MEMBERS_UPDATED_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that the access to the group's members was changed by a remote user. Embeds {{ %1$@ user who changed the access, %2$@ new access level}}.") + addItem(.accessMembers, format: format, updaterName, accessName) + case .unknown: + let format = NSLocalizedString("GROUP_ACCESS_MEMBERS_UPDATED_FORMAT", + comment: "Message indicating that the access to the group's members was changed. Embeds {{new access level}}.") + addItem(.accessMembers, format: format, accessName) + } + } + + if oldAccess.attributes != newAccess.attributes { + let accessName = description(for: newAccess.attributes) + + switch updater { + case .localUser: + let format = NSLocalizedString("GROUP_ACCESS_ATTRIBUTES_UPDATED_BY_LOCAL_USER_FORMAT", + comment: "Message indicating that the access to the group's attributes was changed by the local user. Embeds {{new access level}}.") + addItem(.accessAttributes, format: format, accessName) + case .otherUser(let updaterName, _): + let format = NSLocalizedString("GROUP_ACCESS_ATTRIBUTES_UPDATED_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that the access to the group's attributes was changed by a remote user. Embeds {{ %1$@ user who changed the access, %2$@ new access level}}.") + addItem(.accessAttributes, format: format, updaterName, accessName) + case .unknown: + let format = NSLocalizedString("GROUP_ACCESS_ATTRIBUTES_UPDATED_FORMAT", + comment: "Message indicating that the access to the group's attributes was changed. Embeds {{new access level}}.") + addItem(.accessAttributes, format: format, accessName) + } + } + } + + // MARK: - Membership + + // If Alice and Bob are in a group, and Alice invites Carol, + // Bob is likely to have no info about Carol and won't be + // able to render her name/id. So in some cases we just + // render user "counts", e.g. Bob might see: + // "Alice invited 1 person to the group." + struct MembershipCounts { + var invitedUserCount: UInt = 0 + var inviteRevokedCount: UInt = 0 + } + + func addMembershipUpdates(oldGroupMembership: GroupMembership) { + var membershipCounts = MembershipCounts() + + let allUsers = oldGroupMembership.allUsers.union(newGroupMembership.allUsers) + for address in allUsers { + let oldMembershipStatus = membershipStatus(of: address, in: oldGroupMembership) + let newMembershipStatus = membershipStatus(of: address, in: newGroupMembership) + + switch oldMembershipStatus { + case .normalMember: + switch newMembershipStatus { + case .normalMember: + // Membership status didn't change. + // Check for role changes. + addMemberRoleUpdates(for: address, oldGroupMembership: oldGroupMembership) + case .invited: + owsFailDebug("Invalid membership status transition: \(oldMembershipStatus) -> \(newMembershipStatus).") + break + case .none: + addUserLeftOrWasKickedOutOfGroup(for: address) + } + case .invited: + switch newMembershipStatus { + case .normalMember: + addUserInviteWasAccepted(for: address, + oldGroupMembership: oldGroupMembership) + case .invited: + // Membership status didn't change. + break + case .none: + addUserInviteWasDeclinedOrRevoked(for: address, + oldGroupMembership: oldGroupMembership, + membershipCounts: &membershipCounts) + } + case .none: + switch newMembershipStatus { + case .normalMember: + addUserWasAddedToTheGroup(for: address) + case .invited: + addUserWasInvitedToTheGroup(for: address, + membershipCounts: &membershipCounts) + case .none: + // Membership status didn't change. + break + } + } + } + + // We don't necessarily have profile/contact info for invited + // members, so we render these as counts. + addUnnamedUsersWereInvited(count: membershipCounts.invitedUserCount) + addUnnamedUserInvitesWereRevoked(count: membershipCounts.inviteRevokedCount) + } + + func addMemberRoleUpdates(for address: SignalServiceAddress, + oldGroupMembership: GroupMembership) { + + let oldIsAdministrator = oldGroupMembership.isAdministrator(address) + let newIsAdministrator = newGroupMembership.isAdministrator(address) + + guard oldIsAdministrator != newIsAdministrator else { + // Role didn't change. + return + } + + if newIsAdministrator { + addUserWasGrantedAdministrator(for: address) + } else { + addUserWasRevokedAdministrator(for: address) + } + } + + func addUserWasGrantedAdministrator(for address: SignalServiceAddress) { + let isLocalUser = localAddress == address + if isLocalUser { + switch updater { + case .localUser: + owsFailDebug("Local user made themself administrator.") + addItem(.userRole, + address: address, + copy: NSLocalizedString("GROUP_LOCAL_USER_GRANTED_ADMINISTRATOR", + comment: "Message indicating that the local user was granted administrator role.")) + case .otherUser(let updaterName, let updaterAddress): + if updaterAddress == address { + owsFailDebug("Remote user made themself administrator.") + addItem(.userRole, + address: address, + copy: NSLocalizedString("GROUP_LOCAL_USER_GRANTED_ADMINISTRATOR", + comment: "Message indicating that the local user was granted administrator role.")) + } else { + let format = NSLocalizedString("GROUP_LOCAL_USER_GRANTED_ADMINISTRATOR_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that the local user was granted administrator role by another user. Embeds {{remote user name}}.") + addItem(.userRole, + address: address, + format: format, updaterName) + } + case .unknown: + addItem(.userRole, + address: address, + copy: NSLocalizedString("GROUP_LOCAL_USER_GRANTED_ADMINISTRATOR", + comment: "Message indicating that the local user was granted administrator role.")) + } + } else { + let userName = contactsManager.displayName(for: address, transaction: transaction) + + switch updater { + case .localUser: + let format = NSLocalizedString("GROUP_REMOTE_USER_GRANTED_ADMINISTRATOR_BY_LOCAL_USER", + comment: "Message indicating that a remote user was granted administrator role by local user. Embeds {{remote user name}}.") + addItem(.userRole, + address: address, + format: format, userName) + case .otherUser(let updaterName, let updaterAddress): + if updaterAddress == address { + owsFailDebug("Remote user made themself administrator.") + let format = NSLocalizedString("GROUP_REMOTE_USER_GRANTED_ADMINISTRATOR", + comment: "Message indicating that a remote user was granted administrator role. Embeds {{remote user name}}.") + addItem(.userRole, + address: address, + format: format, userName) + } else { + let format = NSLocalizedString("GROUP_REMOTE_USER_GRANTED_ADMINISTRATOR_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that a remote user was granted administrator role by another user. Embeds {{ %1$@ user who granted, %2$@ user who was granted administrator role}}.") + addItem(.userRole, + address: address, + format: format, updaterName, userName) + } + case .unknown: + let format = NSLocalizedString("GROUP_REMOTE_USER_GRANTED_ADMINISTRATOR", + comment: "Message indicating that a remote user was granted administrator role. Embeds {{remote user name}}.") + addItem(.userRole, + address: address, + format: format, userName) + } + } + } + + func addUserWasRevokedAdministrator(for address: SignalServiceAddress) { + let isLocalUser = localAddress == address + if isLocalUser { + switch updater { + case .localUser: + addItem(.userRole, + address: address, + copy: NSLocalizedString("GROUP_LOCAL_USER_REVOKED_ADMINISTRATOR", + comment: "Message indicating that the local user had their administrator role revoked.")) + case .otherUser(let updaterName, let updaterAddress): + if updaterAddress == address { + addItem(.userRole, + address: address, + copy: NSLocalizedString("GROUP_LOCAL_USER_REVOKED_ADMINISTRATOR", + comment: "Message indicating that the local user had their administrator role revoked.")) + } else { + let format = NSLocalizedString("GROUP_LOCAL_USER_REVOKED_ADMINISTRATOR_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that the local user had their administrator role revoked by another user. Embeds {{remote user name}}.") + addItem(.userRole, + address: address, + format: format, updaterName) + } + case .unknown: + addItem(.userRole, + address: address, + copy: NSLocalizedString("GROUP_LOCAL_USER_REVOKED_ADMINISTRATOR", + comment: "Message indicating that the local user had their administrator role revoked.")) + } + } else { + let userName = contactsManager.displayName(for: address, transaction: transaction) + + switch updater { + case .localUser: + let format = NSLocalizedString("GROUP_REMOTE_USER_REVOKED_ADMINISTRATOR_BY_LOCAL_USER", + comment: "Message indicating that a remote user had their administrator role revoked by local user. Embeds {{remote user name}}.") + addItem(.userRole, + address: address, + format: format, userName) + case .otherUser(let updaterName, let updaterAddress): + if updaterAddress == address { + let format = NSLocalizedString("GROUP_REMOTE_USER_REVOKED_ADMINISTRATOR", + comment: "Message indicating that a remote user had their administrator role revoked. Embeds {{remote user name}}.") + addItem(.userRole, + address: address, + format: format, userName) + } else { + let format = NSLocalizedString("GROUP_REMOTE_USER_REVOKED_ADMINISTRATOR_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that a remote user had their administrator role revoked by another user. Embeds {{ %1$@ user who revoked, %2$@ user who was granted administrator role}}.") + addItem(.userRole, + address: address, + format: format, updaterName, userName) + } + case .unknown: + let format = NSLocalizedString("GROUP_REMOTE_USER_REVOKED_ADMINISTRATOR", + comment: "Message indicating that a remote user had their administrator role revoked. Embeds {{remote user name}}.") + addItem(.userRole, + address: address, + format: format, userName) + } + } + } + + func addUserLeftOrWasKickedOutOfGroup(for address: SignalServiceAddress) { + + let isLocalUser = localAddress == address + if isLocalUser { + // Local user has left or been kicked out of the group. + switch updater { + case .localUser: + addItem(.userMembershipState, + address: address, + copy: NSLocalizedString("GROUP_YOU_LEFT", + comment: "Message indicating that the local user left the group.")) + case .otherUser(let updaterName, _): + let format = NSLocalizedString("GROUP_LOCAL_USER_REMOVED_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that the local user was removed from the group by another user. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, updaterName) + case .unknown: + addItem(.userMembershipState, + address: address, + copy: NSLocalizedString("GROUP_LOCAL_USER_REMOVED_BY_UNKNOWN_USER", + comment: "Message indicating that the local user was removed from the group by an unknown user.")) + } + } else { + let userName = contactsManager.displayName(for: address, transaction: transaction) + + switch updater { + case .localUser: + let format = NSLocalizedString("GROUP_REMOTE_USER_REMOVED_BY_LOCAL_USER_FORMAT", + comment: "Message indicating that a remote user was removed from the group by the local user. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, userName) + case .otherUser(let updaterName, let updaterAddress): + if updaterAddress == address { + let format = NSLocalizedString("GROUP_REMOTE_USER_LEFT_GROUP_FORMAT", + comment: "Message indicating that a remote user has left the group. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, userName) + } else { + let format = NSLocalizedString("GROUP_REMOTE_USER_REMOVED_FROM_GROUP_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that the remote user was removed from the group. Embeds {{ %1$@ user who removed the user, %2$@ user who was removed}}.") + addItem(.userMembershipState, + address: address, + format: format, updaterName, userName) + } + case .unknown: + let format = NSLocalizedString("GROUP_REMOTE_USER_LEFT_GROUP_FORMAT", + comment: "Message indicating that a remote user has left the group. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, userName) + } + } + } + + func addUserInviteWasAccepted(for address: SignalServiceAddress, + oldGroupMembership: GroupMembership) { + + var inviterName: String? + var inviterAddress: SignalServiceAddress? + if let inviterUuid = oldGroupMembership.addedByUuid(forPendingMember: address) { + inviterAddress = SignalServiceAddress(uuid: inviterUuid) + inviterName = contactsManager.displayName(for: SignalServiceAddress(uuid: inviterUuid), + transaction: transaction) + } + + let isLocalUser = localAddress == address + if isLocalUser { + switch updater { + case .localUser: + if let inviterName = inviterName { + let format = NSLocalizedString("GROUP_LOCAL_USER_INVITE_ACCEPTED_FORMAT", + comment: "Message indicating that the local user accepted an invite to the group. Embeds {{user who invited the local user}}.") + addItem(.userMembershipState, + address: address, + format: format, inviterName) + } else { + owsFailDebug("Missing inviter name.") + addItem(.userMembershipState, + address: address, + copy: NSLocalizedString("GROUP_LOCAL_USER_INVITE_ACCEPTED", + comment: "Message indicating that the local user accepted an invite to the group.")) + } + case .otherUser(let updaterName, _): + owsFailDebug("Invite accepted on our behalf.") + let format = NSLocalizedString("GROUP_LOCAL_USER_ADDED_TO_GROUP_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that the local user was added to the group by another user. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, updaterName) + case .unknown: + addItem(.userMembershipState, + address: address, + copy: NSLocalizedString("GROUP_LOCAL_USER_JOINED_THE_GROUP", + comment: "Message indicating that the local user has joined the group.")) + } + } else { + let userName = contactsManager.displayName(for: address, transaction: transaction) + + switch updater { + case .localUser: + owsFailDebug("Invite not accepted by invitee.") + let format = NSLocalizedString("GROUP_REMOTE_USER_ADDED_TO_GROUP_BY_LOCAL_USER_FORMAT", + comment: "Message indicating that a remote user was added to the group by the local user. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, userName) + case .otherUser(let updaterName, let updaterAddress): + if address == updaterAddress { + if inviterAddress == localAddress { + let format = NSLocalizedString("GROUP_REMOTE_USER_ACCEPTED_INVITE_FROM_LOCAL_USER_FORMAT", + comment: "Message indicating that a remote user has accepted an invite from the local user. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, updaterName) + } else if let inviterName = inviterName { + let format = NSLocalizedString("GROUP_REMOTE_USER_ACCEPTED_INVITE_FROM_REMOTE_USER_FORMAT", + comment: "Message indicating that a remote user has accepted their invite. Embeds {{ %1$@ user who accepted their invite, %2$@ user who invited the user}}.") + addItem(.userMembershipState, + address: address, + format: format, updaterName, inviterName) + } else { + owsFailDebug("Missing inviter name.") + let format = NSLocalizedString("GROUP_REMOTE_USER_ACCEPTED_INVITE_FORMAT", + comment: "Message indicating that a remote user has accepted their invite. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, updaterName) + } + } else { + owsFailDebug("Invite accepted by someone other than invitee.") + let format = NSLocalizedString("GROUP_REMOTE_USER_ADDED_TO_GROUP_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that a remote user was added to the group by another user. Embeds {{ %1$@ user who added the user, %2$@ user who was added}}.") + addItem(.userMembershipState, + address: address, + format: format, updaterName, userName) + } + case .unknown: + let format = NSLocalizedString("GROUP_REMOTE_USER_JOINED_GROUP_FORMAT", + comment: "Message indicating that a remote user was added to the group. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, userName) + } + } + } + + func addUserInviteWasDeclinedOrRevoked(for address: SignalServiceAddress, + oldGroupMembership: GroupMembership, + membershipCounts: inout MembershipCounts) { + + var inviterName: String? + var inviterAddress: SignalServiceAddress? + if let inviterUuid = oldGroupMembership.addedByUuid(forPendingMember: address) { + inviterAddress = SignalServiceAddress(uuid: inviterUuid) + inviterName = contactsManager.displayName(for: SignalServiceAddress(uuid: inviterUuid), + transaction: transaction) + } + + let isLocalUser = localAddress == address + if isLocalUser { + switch updater { + case .localUser: + if let inviterName = inviterName { + let format = NSLocalizedString("GROUP_LOCAL_USER_INVITE_DECLINED_FORMAT", + comment: "Message indicating that the local user declined an invite to the group. Embeds {{user who invited the local user}}.") + addItem(.userMembershipState, + address: address, + format: format, inviterName) + } else { + owsFailDebug("Missing inviter name.") + addItem(.userMembershipState, + address: address, + copy: NSLocalizedString("GROUP_LOCAL_USER_INVITE_DECLINED_BY_LOCAL_USER", + comment: "Message indicating that the local user declined an invite to the group.")) + } + case .otherUser(let updaterName, _): + owsFailDebug("Invite revoked.") + let format = NSLocalizedString("GROUP_LOCAL_USER_INVITE_REVOKED_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that the local user's invite was revoked by another user. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, updaterName) + case .unknown: + addItem(.userMembershipState, + address: address, + copy: NSLocalizedString("GROUP_LOCAL_USER_INVITE_REVOKED_BY_UNKNOWN_USER", + comment: "Message indicating that the local user's invite was revoked by an unknown user.")) + } + } else { + let userName = contactsManager.displayName(for: address, transaction: transaction) + + switch updater { + case .localUser: + let format = NSLocalizedString("GROUP_REMOTE_USER_INVITE_REVOKED_BY_LOCAL_USER_FORMAT", + comment: "Message indicating that a remote user's invite was revoked by the local user. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, userName) + case .otherUser(let updaterName, let updaterAddress): + if address == updaterAddress { + if inviterAddress == localAddress { + let format = NSLocalizedString("GROUP_REMOTE_USER_DECLINED_INVITE_FROM_LOCAL_USER_FORMAT", + comment: "Message indicating that a remote user has declined an invite to the group from the local user. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, updaterName) + } else if let inviterName = inviterName { + let format = NSLocalizedString("GROUP_REMOTE_USER_DECLINED_INVITE_FORMAT", + comment: "Message indicating that a remote user has declined their invite. Embeds {{ user who invited them }}.") + addItem(.userMembershipState, + address: address, + format: format, inviterName) + } else { + addItem(.userMembershipState, + address: address, + copy: NSLocalizedString("GROUP_REMOTE_USER_DECLINED_INVITE", + comment: "Message indicating that a remote user has declined their invite.")) + } + } else { + membershipCounts.inviteRevokedCount += 1 + } + case .unknown: + membershipCounts.inviteRevokedCount += 1 + } + } + } + + func addUnnamedUserInvitesWereRevoked(count: UInt) { + guard count > 0 else { + return + } + let countString = String.localizedStringWithFormat("%d", count) + + switch updater { + case .localUser: + owsFailDebug("Unexpected updater.") + case .otherUser(let updaterName, _): + if count == 1 { + let format = NSLocalizedString("GROUP_REMOTE_USER_INVITE_REVOKED_BY_REMOTE_USER_1_FORMAT", + comment: "Message indicating that a single remote user's invite was revoked by a remote user. Embeds {{ user who revoked the invite }}.") + addItem(.userMembershipState_invitesRevoked, + format: format, updaterName, countString) + } else { + let format = NSLocalizedString("GROUP_REMOTE_USER_INVITE_REVOKED_BY_REMOTE_USER_N_FORMAT", + comment: "Message indicating that a group of remote users' invites were revoked by a remote user. Embeds {{ %1$@ user who revoked the invite, %2$@ number of users }}.") + addItem(.userMembershipState_invitesRevoked, + format: format, updaterName, countString) + } + case .unknown: + if count == 1 { + addItem(.userMembershipState_invitesRevoked, + copy: NSLocalizedString("GROUP_REMOTE_USER_INVITE_REVOKED_1", + comment: "Message indicating that a single remote user's invite was revoked.")) + } else { + let format = NSLocalizedString("GROUP_REMOTE_USER_INVITE_REVOKED_N_FORMAT", + comment: "Message indicating that a group of remote users' invites were revoked. Embeds {{ number of users }}.") + addItem(.userMembershipState_invitesRevoked, + format: format, countString) + } + } + } + + func addUserWasAddedToTheGroup(for address: SignalServiceAddress) { + + let isLocalUser = localAddress == address + if isLocalUser { + switch updater { + case .localUser: + owsFailDebug("User added themself to the group.") + addItem(.userMembershipState, + address: address, + copy: NSLocalizedString("GROUP_LOCAL_USER_JOINED_THE_GROUP", + comment: "Message indicating that the local user has joined the group.")) + case .otherUser(let updaterName, _): + let format = NSLocalizedString("GROUP_LOCAL_USER_ADDED_TO_GROUP_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that the local user was added to the group by another user. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, updaterName) + case .unknown: + addItem(.userMembershipState, + address: address, + copy: NSLocalizedString("GROUP_LOCAL_USER_JOINED_THE_GROUP", + comment: "Message indicating that the local user has joined the group.")) + } + } else { + let userName = contactsManager.displayName(for: address, transaction: transaction) + + switch updater { + case .localUser: + let format = NSLocalizedString("GROUP_REMOTE_USER_ADDED_TO_GROUP_BY_LOCAL_USER_FORMAT", + comment: "Message indicating that a remote user was added to the group by the local user. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, userName) + case .otherUser(let updaterName, let updaterAddress): + if address == updaterAddress { + if newGroupModel.groupsVersion == .V2 { + owsFailDebug("Remote user added themself to the group.") + } + let format = NSLocalizedString("GROUP_REMOTE_USER_ADDED_TO_GROUP_FORMAT", + comment: "Message indicating that a remote user was added to the group. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, userName) + } else { + let format = NSLocalizedString("GROUP_REMOTE_USER_ADDED_TO_GROUP_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that a remote user was added to the group by another user. Embeds {{ %1$@ user who added the user, %2$@ user who was added}}.") + addItem(.userMembershipState, + address: address, + format: format, updaterName, userName) + } + case .unknown: + let format = NSLocalizedString("GROUP_REMOTE_USER_ADDED_TO_GROUP_FORMAT", + comment: "Message indicating that a remote user was added to the group. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, userName) + } + } + } + + func addUserWasInvitedToTheGroup(for address: SignalServiceAddress, + membershipCounts: inout MembershipCounts) { + + let isLocalUser = localAddress == address + if isLocalUser { + switch updater { + case .localUser: + owsFailDebug("User invited themself to the group.") + addItem(.userMembershipState, + address: address, + copy: NSLocalizedString("GROUP_LOCAL_USER_INVITED_TO_THE_GROUP", + comment: "Message indicating that the local user was invited to the group.")) + case .otherUser(let updaterName, _): + let format = NSLocalizedString("GROUP_LOCAL_USER_INVITED_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that the local user was invited to the group by another user. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, updaterName) + case .unknown: + addItem(.userMembershipState, + address: address, + copy: NSLocalizedString("GROUP_LOCAL_USER_INVITED_TO_THE_GROUP", + comment: "Message indicating that the local user was invited to the group.")) + } + } else { + let userName = contactsManager.displayName(for: address, transaction: transaction) + + switch updater { + case .localUser: + let format = NSLocalizedString("GROUP_REMOTE_USER_INVITED_BY_LOCAL_USER_FORMAT", + comment: "Message indicating that a remote user was invited to the group by the local user. Embeds {{remote user name}}.") + addItem(.userMembershipState, + address: address, + format: format, userName) + default: + membershipCounts.invitedUserCount += 1 + } + } + } + + func addUnnamedUsersWereInvited(count: UInt) { + guard count > 0 else { + return + } + let countString = String.localizedStringWithFormat("%d", count) + + switch updater { + case .localUser: + owsFailDebug("Unexpected updater.") + case .otherUser(let updaterName, _): + 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 }}.") + addItem(.userMembershipState_invitesNew, + format: format, updaterName, countString) + } else { + let format = NSLocalizedString("GROUP_REMOTE_USER_INVITED_BY_REMOTE_USER_N_FORMAT", + comment: "Message indicating that a group of remote users were invited to the group by the local user. Embeds {{ %1$@ user who invited the user, %2$@ number of invited users }}.") + addItem(.userMembershipState_invitesNew, + format: format, updaterName, countString) + } + case .unknown: + if count == 1 { + addItem(.userMembershipState_invitesNew, + copy: NSLocalizedString("GROUP_REMOTE_USER_INVITED_1", + comment: "Message indicating that a single remote user was invited to the group.")) + } else { + let format = NSLocalizedString("GROUP_REMOTE_USER_INVITED_N_FORMAT", + comment: "Message indicating that a group of remote users were invited to the group. Embeds {{number of invited users}}.") + addItem(.userMembershipState_invitesNew, + format: format, countString) + } + } + } + + // MARK: - Disappearing Messages + + func addDisappearingMessageUpdates(oldToken: DisappearingMessageToken?, + newToken: DisappearingMessageToken?) { + + guard let newToken = newToken else { + // This info message was created before we embedded DM state. + return + } + + // This might be zero if DMs are not enabled. + let durationString = NSString.formatDurationSeconds(newToken.durationSeconds, useShortFormat: false) + + guard let oldToken = oldToken else { + if newToken.isEnabled { + let format = NSLocalizedString("DISAPPEARING_MESSAGES_CONFIGURATION_GROUP_EXISTING_FORMAT", + comment: "Info Message when added to a group which has enabled disappearing messages. Embeds {{time amount}} before messages disappear. See the *_TIME_AMOUNT strings for context.") + addItem(.disappearingMessagesState, format: format, durationString) + } + return + } + + guard newToken != oldToken else { + // No change to disappearing message configuration occurred. + return + } + + switch updater { + case .localUser: + // Changed by localNumber on this device or via synced transcript + if newToken.isEnabled { + let format = NSLocalizedString("YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION", + comment: "Info Message when you disabled disappearing messages. Embeds a {{time amount}} before messages disappear. see the *_TIME_AMOUNT strings for context.") + addItem(.disappearingMessagesState, format: format, durationString) + } else { + addItem(.disappearingMessagesState, + copy: NSLocalizedString("YOU_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION", + comment: "Info Message when you disabled disappearing messages.")) + } + case .otherUser(let updaterName, _): + if newToken.isEnabled { + let format = NSLocalizedString("OTHER_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION", + comment: "Info Message when another user enabled disappearing messages. Embeds {{name of other user}} and {{time amount}} before messages disappear. See the *_TIME_AMOUNT strings for context.") + addItem(.disappearingMessagesState, format: format, updaterName, durationString) + } else { + let format = NSLocalizedString("OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION", + comment: "Info Message when another user disabled disappearing messages. Embeds {{name of other user}}.") + addItem(.disappearingMessagesState, format: format, updaterName) + } + case .unknown: + // Changed by unknown user. + if newToken.isEnabled { + let format = NSLocalizedString("UNKNOWN_USER_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION", + comment: "Info Message when an unknown user enabled disappearing messages. Embeds {{time amount}} before messages disappear. see the *_TIME_AMOUNT strings for context.") + addItem(.disappearingMessagesState, format: format, durationString) + } else { + addItem(.disappearingMessagesState, + copy: NSLocalizedString("UNKNOWN_USER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION", + comment: "Info Message when an unknown user disabled disappearing messages.")) + } + } + } + + func addGroupWasUpserted() { + guard newGroupModel.groupsVersion == .V2 else { + // Group was just upserted. + switch updater { + case .localUser: + addItem(.groupCreated, + copy: NSLocalizedString("GROUP_CREATED_BY_LOCAL_USER", + comment: "Message indicating that group was created by the local user.")) + default: + // Unless we know we created the group, + // we don't know if the group was just created + // or if we were just added to it, so use the + // old generic description. + addItem(.groupCreated, + copy: defaultGroupUpdateDescription) + } + return + } + + let wasGroupJustCreated = newGroupModel.groupV2Revision == 0 + if wasGroupJustCreated { + // Group was just created. + switch updater { + case .localUser: + addItem(.groupCreated, + copy: NSLocalizedString("GROUP_CREATED_BY_LOCAL_USER", + comment: "Message indicating that group was created by the local user.")) + case .otherUser(let updaterName, _): + let format = NSLocalizedString("GROUP_CREATED_BY_REMOTE_USER_FORMAT", + comment: "Message indicating that group was created by another user. Embeds {{remote user name}}.") + addItem(.groupCreated, format: format, updaterName) + case .unknown: + addItem(.groupCreated, + copy: NSLocalizedString("GROUP_CREATED_BY_UNKNOWN_USER", + comment: "Message indicating that group was created by an unknown user.")) + } + } + + switch localMembershipStatus(for: newGroupMembership) { + case .normalMember: + guard !wasGroupJustCreated else { + // If group was just created, it's implicit that we were added. + return + } + addItem(.userMembershipState, + address: localAddress, + copy: NSLocalizedString("GROUP_LOCAL_USER_JOINED_THE_GROUP", + comment: "Message indicating that the local user has joined the group.")) + case .invited: + addItem(.userMembershipState, + address: localAddress, + copy: NSLocalizedString("GROUP_LOCAL_USER_INVITED_TO_THE_GROUP", + comment: "Message indicating that the local user was invited to the group.")) + case .none: + owsFailDebug("Learned of group without any membership status.") + } + } + + // MARK: - Membership Status + + enum MembershipStatus { + case normalMember + case invited + case none + } + + func localMembershipStatus(for groupMembership: GroupMembership) -> MembershipStatus { + return membershipStatus(of: localAddress, in: groupMembership) + } + + func membershipStatus(of address: SignalServiceAddress, + in groupMembership: GroupMembership) -> MembershipStatus { + if groupMembership.isNonPendingMember(address) { + return .normalMember + } else if groupMembership.isPending(address) { + return .invited + } else { + return .none + } + } + + // MARK: - Updater + + enum Updater { + case localUser + case otherUser(updaterName: String, updaterAddress: SignalServiceAddress) + case unknown + } + + static func updater(groupUpdateSourceAddress: SignalServiceAddress?, + transaction: SDSAnyReadTransaction) -> Updater { + guard let updaterAddress = groupUpdateSourceAddress else { + return .unknown + } + guard let localAddress = tsAccountManager.localAddress else { + owsFailDebug("missing local address") + return .unknown + } + if localAddress == updaterAddress { + return .localUser + } + + let updaterName = contactsManager.displayName(for: updaterAddress, transaction: transaction) + return .otherUser(updaterName: updaterName, updaterAddress: updaterAddress) + } + + // MARK: - Defaults + + var defaultGroupUpdateDescription: String { + return GroupUpdate.defaultGroupUpdateDescription(groupUpdateSourceAddress: groupUpdateSourceAddress, + transaction: transaction) + } + + static func defaultGroupUpdateDescription(groupUpdateSourceAddress: SignalServiceAddress?, + transaction: SDSAnyReadTransaction) -> String { + let updater = GroupUpdate.updater(groupUpdateSourceAddress: groupUpdateSourceAddress, + transaction: transaction) + switch updater { + case .localUser: + return NSLocalizedString("GROUP_UPDATED_BY_LOCAL_USER", + comment: "Info message indicating that the group was updated by the local user.") + case .otherUser(let updaterName, _): + let format = NSLocalizedString("GROUP_UPDATED_BY_REMOTE_USER_FORMAT", + comment: "Info message indicating that the group was updated by another user. Embeds {{remote user name}}.") + return String(format: format, updaterName) + case .unknown: + return NSLocalizedString("GROUP_UPDATED", + comment: "Info message indicating that the group was updated by an unknown user.") + } } } diff --git a/SignalMessaging/Views/ImageEditor/OrderedDictionary.swift b/SignalServiceKit/src/Util/OrderedDictionary.swift similarity index 72% rename from SignalMessaging/Views/ImageEditor/OrderedDictionary.swift rename to SignalServiceKit/src/Util/OrderedDictionary.swift index 5cdd820c11..039bebb3e2 100644 --- a/SignalMessaging/Views/ImageEditor/OrderedDictionary.swift +++ b/SignalServiceKit/src/Util/OrderedDictionary.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// Copyright (c) 2020 Open Whisper Systems. All rights reserved. // import Foundation @@ -103,3 +103,38 @@ public class OrderedDictionary { return values } } + +// MARK: - + +extension OrderedDictionary: Sequence { + public typealias Iterator = AnyIterator<(KeyType, ValueType)> + + struct OrderedDictionaryIterator { + private let keys: [KeyType] + private let map: [KeyType: ValueType] + private var index: Int = 0 + + fileprivate init(keys: [KeyType], map: [KeyType: ValueType]) { + self.keys = keys + self.map = map + } + + mutating func next() -> (KeyType, ValueType)? { + guard index < keys.count else { + return nil + } + let key = keys[index] + index += 1 + guard let value = map[key] else { + owsFailDebug("Missing value for key.") + return nil + } + return (key, value) + } + } + + public func makeIterator() -> Iterator { + var iterator = OrderedDictionaryIterator(keys: orderedKeys, map: keyValueMap) + return AnyIterator { iterator.next() } + } +}