From c82ee0c44bf16060264e73dedcc42114a5b5d995 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 26 Aug 2022 14:09:41 +1000 Subject: [PATCH] Finished off the updated settings sections and fixed a couple of bugs Added the Blocked contacts screen Added a setting to control whether concurrent audio messages should auto-play Finished updating the settings screens Fixed an issue where items that should be removed from the PagedDatabaseObserver due to filter logic weren't getting removed in some cases --- Session.xcodeproj/project.pbxproj | 24 +- .../ConversationVC+Interaction.swift | 135 ++++--- Session/Conversations/ConversationVC.swift | 6 + .../Conversations/ConversationViewModel.swift | 4 +- .../Content Views/TypingIndicatorView.swift | 15 +- .../Message Cells/TypingIndicatorCell.swift | 4 +- .../OWSConversationSettingsViewController.m | 7 +- Session/Home/HomeVC.swift | 81 ++-- Session/Home/HomeViewModel.swift | 10 + .../MessageRequestsViewController.swift | 15 +- Session/Meta/AppDelegate.swift | 2 +- .../Translations/de.lproj/Localizable.strings | 18 +- .../Translations/en.lproj/Localizable.strings | 18 +- .../Translations/es.lproj/Localizable.strings | 18 +- .../Translations/fa.lproj/Localizable.strings | 18 +- .../Translations/fi.lproj/Localizable.strings | 18 +- .../Translations/fr.lproj/Localizable.strings | 18 +- .../Translations/hi.lproj/Localizable.strings | 18 +- .../Translations/hr.lproj/Localizable.strings | 18 +- .../id-ID.lproj/Localizable.strings | 18 +- .../Translations/it.lproj/Localizable.strings | 18 +- .../Translations/ja.lproj/Localizable.strings | 18 +- .../Translations/nl.lproj/Localizable.strings | 18 +- .../Translations/pl.lproj/Localizable.strings | 18 +- .../pt_BR.lproj/Localizable.strings | 18 +- .../Translations/ru.lproj/Localizable.strings | 18 +- .../Translations/si.lproj/Localizable.strings | 18 +- .../Translations/sk.lproj/Localizable.strings | 18 +- .../Translations/sv.lproj/Localizable.strings | 18 +- .../Translations/th.lproj/Localizable.strings | 18 +- .../vi-VN.lproj/Localizable.strings | 18 +- .../zh-Hant.lproj/Localizable.strings | 18 +- .../zh_CN.lproj/Localizable.strings | 18 +- Session/Path/PathVC.swift | 105 ++--- .../BlockedContactsViewController.swift | 382 ++++++++++++++++++ .../Settings/BlockedContactsViewModel.swift | 214 ++++++++++ .../ConversationSettingsViewController.swift | 62 --- .../ConversationSettingsViewModel.swift | 91 +++++ Session/Settings/HelpViewModel.swift | 53 ++- .../Settings/NotificationSoundViewModel.swift | 19 + Session/Settings/NukeDataModal.swift | 1 - .../Settings/PrivacySettingsViewModel.swift | 26 ++ Session/Settings/SeedModal.swift | 157 ++++--- .../SettingsTableViewController.swift | 28 +- Session/Settings/SettingsTableViewModel.swift | 16 +- Session/Settings/SettingsVC.swift | 36 +- Session/Settings/ShareLogsModal.swift | 103 ----- .../Settings/Views/BlockedContactCell.swift | 90 +++++ Session/Settings/Views/SettingsCell.swift | 83 +++- .../Settings/Views/ThemeSelectionView.swift | 53 +-- Session/Shared/FullConversationCell.swift | 20 + Session/Sheets & Modals/Modal.swift | 10 +- .../Shared Models/MessageViewModel.swift | 18 +- .../SessionThreadViewModel.swift | 24 +- .../Utilities/Preferences.swift | 4 + SessionUIKit/Components/OutlineButton.swift | 8 + .../Style Guide/Themes/UIKit+Theme.swift | 2 +- .../Types/PagedDatabaseObserver.swift | 45 ++- 58 files changed, 1783 insertions(+), 566 deletions(-) create mode 100644 Session/Settings/BlockedContactsViewController.swift create mode 100644 Session/Settings/BlockedContactsViewModel.swift delete mode 100644 Session/Settings/ConversationSettingsViewController.swift create mode 100644 Session/Settings/ConversationSettingsViewModel.swift delete mode 100644 Session/Settings/ShareLogsModal.swift create mode 100644 Session/Settings/Views/BlockedContactCell.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 13306230c..037513cd2 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -125,7 +125,6 @@ 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; }; 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; }; 7B7CB189270430D20079FF93 /* CallMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB188270430D20079FF93 /* CallMessageView.swift */; }; - 7B7CB18B270591630079FF93 /* ShareLogsModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18A270591630079FF93 /* ShareLogsModal.swift */; }; 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; }; 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */; }; 7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */; }; @@ -753,6 +752,10 @@ FD859EFC27C2F60700510D0C /* MockEd25519.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EFB27C2F60700510D0C /* MockEd25519.swift */; }; FD86585828507B24008B6CF9 /* NSData+messagePadding.m in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D4825589FF20043A11F /* NSData+messagePadding.m */; }; FD87DD0428B8727D00AF0F98 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DD0328B8727D00AF0F98 /* Configuration.swift */; }; + FD87DCFA28B74DB300AF0F98 /* ConversationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */; }; + FD87DCFC28B755B800AF0F98 /* BlockedContactsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFB28B755B800AF0F98 /* BlockedContactsViewController.swift */; }; + FD87DCFE28B7582C00AF0F98 /* BlockedContactsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */; }; + FD87DD0028B820F200AF0F98 /* BlockedContactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */; }; FD90040F2818AB6D00ABAAF6 /* GetSnodePoolJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */; }; FD9004122818ABDC00ABAAF6 /* Job.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73F280402C4004C14C5 /* Job.swift */; }; FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */; }; @@ -822,7 +825,7 @@ FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; }; FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; }; FDE72118286C156E0093DF33 /* ConversationSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE72117286C156E0093DF33 /* ConversationSettingsViewController.swift */; }; - FDE72154287FE4470093DF33 /* (null) in Sources */ = {isa = PBXBuildFile; }; + FDE72154287FE4470093DF33 /* HighlightMentionBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE72153287FE4470093DF33 /* HighlightMentionBackgroundView.swift */; }; FDE77F6B280FEB28002CFC5D /* ControlMessageProcessRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */; }; FDED2E3C282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDED2E3B282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift */; }; FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */; }; @@ -1188,7 +1191,6 @@ 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = ""; }; 7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = ""; }; 7B7CB188270430D20079FF93 /* CallMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessageView.swift; sourceTree = ""; }; - 7B7CB18A270591630079FF93 /* ShareLogsModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLogsModal.swift; sourceTree = ""; }; 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = ""; }; 7B7CB18F270FB2150079FF93 /* MiniCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MiniCallView.swift; sourceTree = ""; }; 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRingTonePlayer.swift; sourceTree = ""; }; @@ -1836,6 +1838,10 @@ FD859EF927C2F5C500510D0C /* MockGenericHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGenericHash.swift; sourceTree = ""; }; FD859EFB27C2F60700510D0C /* MockEd25519.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockEd25519.swift; sourceTree = ""; }; FD87DD0328B8727D00AF0F98 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; + FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSettingsViewModel.swift; sourceTree = ""; }; + FD87DCFB28B755B800AF0F98 /* BlockedContactsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactsViewController.swift; sourceTree = ""; }; + FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactsViewModel.swift; sourceTree = ""; }; + FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactCell.swift; sourceTree = ""; }; FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSnodePoolJob.swift; sourceTree = ""; }; FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; FDA8EAFD280E8B78002B68E5 /* FailedMessageSendsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedMessageSendsJob.swift; sourceTree = ""; }; @@ -1898,7 +1904,6 @@ FDD2506D283711D600198BDA /* DifferenceKit+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DifferenceKit+Utilities.swift"; sourceTree = ""; }; FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = ""; }; FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = ""; }; - FDE72117286C156E0093DF33 /* ConversationSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSettingsViewController.swift; sourceTree = ""; }; FDE7214F287E50D50093DF33 /* ProtoWrappers.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ProtoWrappers.py; sourceTree = ""; }; FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LintLocalizableStrings.swift; sourceTree = ""; }; FDE77F68280F9EDA002CFC5D /* JobRunnerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerError.swift; sourceTree = ""; }; @@ -2911,11 +2916,12 @@ FD37EA0428AA00C1003AE748 /* NotificationSettingsViewModel.swift */, FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */, FD37EA1628AC5605003AE748 /* NotificationContentViewModel.swift */, + FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */, + FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */, + FD87DCFB28B755B800AF0F98 /* BlockedContactsViewController.swift */, FD37E9CB28A1E578003AE748 /* AppearanceViewController.swift */, - FDE72117286C156E0093DF33 /* ConversationSettingsViewController.swift */, FD37EA0228A9FDCC003AE748 /* HelpViewModel.swift */, B86BD08523399CEF000F5AE3 /* SeedModal.swift */, - 7B7CB18A270591630079FF93 /* ShareLogsModal.swift */, B894D0742339EDCF00B4D94D /* NukeDataModal.swift */, ); path = Settings; @@ -3723,6 +3729,7 @@ FD37E9DA28A244E9003AE748 /* ThemePreviewView.swift */, FD37E9DC28A384EB003AE748 /* PrimaryColorSelectionView.swift */, FD37EA0A28AB12E2003AE748 /* SettingsCell.swift */, + FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */, ); path = Views; sourceTree = ""; @@ -5502,7 +5509,6 @@ 34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */, 34A8B3512190A40E00218A25 /* MediaAlbumView.swift in Sources */, FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */, - 7B7CB18B270591630079FF93 /* ShareLogsModal.swift in Sources */, 4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */, 3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */, C3A76A8D25DB83F90074CB90 /* PermissionMissingModal.swift in Sources */, @@ -5519,6 +5525,7 @@ B8D0A26925E4A2C200C1835E /* Onboarding.swift in Sources */, 34D1F0521F7E8EA30066283D /* GiphyDownloader.swift in Sources */, 4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */, + FD87DCFE28B7582C00AF0F98 /* BlockedContactsViewModel.swift in Sources */, FD37E9DD28A384EB003AE748 /* PrimaryColorSelectionView.swift in Sources */, B82149B825D60393009C0F2A /* BlockedModal.swift in Sources */, B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */, @@ -5563,6 +5570,7 @@ FD37EA1728AC5605003AE748 /* NotificationContentViewModel.swift in Sources */, B886B4A72398B23E00211ABE /* QRCodeVC.swift in Sources */, 4C586926224FAB83003FD070 /* AVAudioSession+OWS.m in Sources */, + FD87DD0028B820F200AF0F98 /* BlockedContactCell.swift in Sources */, C331FFF42558FF0300070591 /* PNOptionView.swift in Sources */, 4C4AE6A1224AF35700D4AF6F /* SendMediaNavigationController.swift in Sources */, B82149C125D605C6009C0F2A /* InfoBanner.swift in Sources */, @@ -5580,10 +5588,12 @@ B8D84ECF25E3108A005A043E /* ExpandingAttachmentsButton.swift in Sources */, 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */, B875885A264503A6000E60D0 /* JoinOpenGroupModal.swift in Sources */, + FD87DCFC28B755B800AF0F98 /* BlockedContactsViewController.swift in Sources */, B8CCF6432397711F0091D419 /* SettingsVC.swift in Sources */, 7B13E1E92810F01300BD4F64 /* SessionCallManager+Action.swift in Sources */, C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */, 7B1581E827210ECC00848B49 /* RenderView.swift in Sources */, + FD87DCFA28B74DB300AF0F98 /* ConversationSettingsViewModel.swift in Sources */, 7BC707F227290ACB002817AD /* SessionCallManager.swift in Sources */, 7BAADFCE27B215FE007BCF92 /* UIView+Draggable.swift in Sources */, 45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index dfc49ad15..1801cfce9 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1146,58 +1146,7 @@ extension ConversationVC: .filter(id: cellViewModel.id) .asRequest(of: Int64.self) .fetchOne(db) - else { - // If the message hasn't been sent yet then just delete locally - guard cellViewModel.state == .sending || cellViewModel.state == .failed else { return } - - // Retrieve any message send jobs for this interaction - let jobs: [Job] = Storage.shared - .read { db in - try? Job - .filter(Job.Columns.variant == Job.Variant.messageSend) - .filter(Job.Columns.interactionId == cellViewModel.id) - .fetchAll(db) - } - .defaulting(to: []) - - // If the job is currently running then wait until it's done before triggering - // the deletion - let targetJob: Job? = jobs.first(where: { JobRunner.isCurrentlyRunning($0) }) - - guard targetJob == nil else { - JobRunner.afterCurrentlyRunningJob(targetJob) { [weak self] result in - switch result { - // If it succeeded then we'll need to delete from the server so re-run - // this function (if we still don't have the server id for some reason - // then this would result in a local-only deletion which should be fine - case .succeeded: self?.delete(cellViewModel) - - // Otherwise we just need to cancel the pending job (in case it retries) - // and delete the interaction - default: - JobRunner.removePendingJob(targetJob) - - Storage.shared.writeAsync { db in - _ = try Interaction - .filter(id: cellViewModel.id) - .deleteAll(db) - } - } - } - return - } - - // If it's not currently running then remove any pending jobs (just to be safe) and - // delete the interaction locally - jobs.forEach { JobRunner.removePendingJob($0) } - - Storage.shared.writeAsync { db in - _ = try Interaction - .filter(id: cellViewModel.id) - .deleteAll(db) - } - return - } + else { return } if remove { OpenGroupAPI @@ -1209,7 +1158,8 @@ extension ConversationVC: on: openGroup.server ) .retainUntilComplete() - } else { + } + else { OpenGroupAPI .reactionAdd( db, @@ -1220,8 +1170,8 @@ extension ConversationVC: ) .retainUntilComplete() } - - } else { + } + else { // Send the actual message try MessageSender.send( db, @@ -1446,7 +1396,58 @@ extension ConversationVC: on: openGroup.server ) ) - else { return } + else { + // If the message hasn't been sent yet then just delete locally + guard cellViewModel.state == .sending || cellViewModel.state == .failed else { return } + + // Retrieve any message send jobs for this interaction + let jobs: [Job] = Storage.shared + .read { db in + try? Job + .filter(Job.Columns.variant == Job.Variant.messageSend) + .filter(Job.Columns.interactionId == cellViewModel.id) + .fetchAll(db) + } + .defaulting(to: []) + + // If the job is currently running then wait until it's done before triggering + // the deletion + let targetJob: Job? = jobs.first(where: { JobRunner.isCurrentlyRunning($0) }) + + guard targetJob == nil else { + JobRunner.afterCurrentlyRunningJob(targetJob) { [weak self] result in + switch result { + // If it succeeded then we'll need to delete from the server so re-run + // this function (if we still don't have the server id for some reason + // then this would result in a local-only deletion which should be fine + case .succeeded: self?.delete(cellViewModel) + + // Otherwise we just need to cancel the pending job (in case it retries) + // and delete the interaction + default: + JobRunner.removePendingJob(targetJob) + + Storage.shared.writeAsync { db in + _ = try Interaction + .filter(id: cellViewModel.id) + .deleteAll(db) + } + } + } + return + } + + // If it's not currently running then remove any pending jobs (just to be safe) and + // delete the interaction locally + jobs.forEach { JobRunner.removePendingJob($0) } + + Storage.shared.writeAsync { db in + _ = try Interaction + .filter(id: cellViewModel.id) + .deleteAll(db) + } + return + } // Delete the message from the open group deleteRemotely( @@ -1921,6 +1922,28 @@ extension ConversationVC: default: return } } + + // MARK: - Data Extraction Notifications + + @objc func sendScreenshotNotification() { + // Only send screenshot notifications to one-to-one conversations + guard self.viewModel.threadData.threadVariant == .contact else { return } + + let threadId: String = self.viewModel.threadData.threadId + + Storage.shared.writeAsync { db in + guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { return } + + try MessageSender.send( + db, + message: DataExtractionNotification( + kind: .screenshot + ), + interactionId: nil, + in: thread + ) + } + } // MARK: - Convenience diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 4aae5bffd..58637be39 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -365,6 +365,12 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers name: UIResponder.keyboardWillHideNotification, object: nil ) + NotificationCenter.default.addObserver( + self, + selector: #selector(sendScreenshotNotification), + name: UIApplication.userDidTakeScreenshotNotification, + object: nil + ) } override func viewWillAppear(_ animated: Bool) { diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index 757c53e26..18cb8ce50 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -198,6 +198,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { }() ) ], + joinSQL: MessageViewModel.optimisedJoinSQL, filterSQL: MessageViewModel.filterSQL(threadId: threadId), groupSQL: MessageViewModel.groupSQL, orderSQL: MessageViewModel.orderSQL, @@ -714,7 +715,8 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { let currentIndex: Int = messageSection.elements .firstIndex(where: { $0.id == interactionId }), currentIndex < (messageSection.elements.count - 1), - messageSection.elements[currentIndex + 1].cellType == .audio + messageSection.elements[currentIndex + 1].cellType == .audio, + Storage.shared[.shouldAutoPlayConsecutiveAudioMessages] == true else { return } let nextItem: MessageViewModel = messageSection.elements[currentIndex + 1] diff --git a/Session/Conversations/Message Cells/Content Views/TypingIndicatorView.swift b/Session/Conversations/Message Cells/Content Views/TypingIndicatorView.swift index 0288b78bf..f7ca60f1e 100644 --- a/Session/Conversations/Message Cells/Content Views/TypingIndicatorView.swift +++ b/Session/Conversations/Message Cells/Content Views/TypingIndicatorView.swift @@ -1,6 +1,7 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUIKit @objc class TypingIndicatorView: UIStackView { // This represents the spacing between the dots @@ -122,12 +123,18 @@ autoSetDimension(.height, toSize: kMaxRadiusPt) layer.addSublayer(shapeLayer) + + ThemeManager.onThemeChange(observer: self) { [weak self] _, _ in + guard self?.shapeLayer.animationKeys()?.isEmpty == false else { return } + + self?.startAnimation() + } } fileprivate func startAnimation() { stopAnimation() - let baseColor = Colors.text + let baseColor: UIColor = (ThemeManager.currentTheme.colors[.messageBubble_incomingText] ?? .white) let timeIncrement: CFTimeInterval = 0.15 var colorValues = [CGColor]() var pathValues = [CGPath]() diff --git a/Session/Conversations/Message Cells/TypingIndicatorCell.swift b/Session/Conversations/Message Cells/TypingIndicatorCell.swift index b4fcc38f8..eb6ff8169 100644 --- a/Session/Conversations/Message Cells/TypingIndicatorCell.swift +++ b/Session/Conversations/Message Cells/TypingIndicatorCell.swift @@ -13,14 +13,14 @@ final class TypingIndicatorCell: MessageCell { private lazy var bubbleView: UIView = { let result: UIView = UIView() result.layer.cornerRadius = VisibleMessageCell.smallCornerRadius - result.backgroundColor = Colors.receivedMessageBackground + result.themeBackgroundColor = .messageBubble_incomingBackground return result }() private let bubbleViewMaskLayer: CAShapeLayer = CAShapeLayer() - private lazy var typingIndicatorView: TypingIndicatorView = TypingIndicatorView() + public lazy var typingIndicatorView: TypingIndicatorView = TypingIndicatorView() // MARK: - Lifecycle diff --git a/Session/Conversations/Settings/OWSConversationSettingsViewController.m b/Session/Conversations/Settings/OWSConversationSettingsViewController.m index 8af9dc453..ea4106e91 100644 --- a/Session/Conversations/Settings/OWSConversationSettingsViewController.m +++ b/Session/Conversations/Settings/OWSConversationSettingsViewController.m @@ -3,7 +3,6 @@ // #import "OWSConversationSettingsViewController.h" -#import "OWSSoundSettingsViewController.h" #import "Session-Swift.h" #import "UIFont+OWS.h" #import "UIView+OWS.h" @@ -417,9 +416,9 @@ CGFloat kIconViewLength = 24; } customRowHeight:UITableViewAutomaticDimension actionBlock:^{ - OWSSoundSettingsViewController *vc = [OWSSoundSettingsViewController new]; - vc.threadId = weakSelf.threadId; - [weakSelf.navigationController pushViewController:vc animated:YES]; + // FIXME: Remove `threadId` once we ditch the per-thread notification sound + UIViewController *viewController = [OWSNotificationSoundSettings createWith:weakSelf.threadId]; + [weakSelf.navigationController pushViewController:viewController animated:YES]; }]]; if (self.isClosedGroup || self.isOpenGroup) { diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index b2cbf0ae2..253701eac 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -527,24 +527,26 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve return true } - func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let section: HomeViewModel.SectionModel = self.viewModel.threadData[indexPath.section] + let unswipeAnimationDelay: DispatchTimeInterval = .milliseconds(500) switch section.model { case .messageRequests: - let hide = UITableViewRowAction(style: .destructive, title: "TXT_HIDE_TITLE".localized()) { _, _ in + let hide: UIContextualAction = UIContextualAction(style: .destructive, title: "TXT_HIDE_TITLE".localized()) { _, _, completionHandler in Storage.shared.write { db in db[.hasHiddenMessageRequests] = true } + completionHandler(true) } hide.themeBackgroundColor = .conversationButton_swipeDestructive - return [hide] + return UISwipeActionsConfiguration(actions: [hide]) case .threads: let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row] - let delete: UITableViewRowAction = UITableViewRowAction( + let delete: UIContextualAction = UIContextualAction( style: .destructive, title: "TXT_DELETE_TITLE".localized() - ) { [weak self] _, _ in + ) { [weak self] _, _, completionHandler in let message = (threadViewModel.currentUserIsClosedGroupAdmin == true ? "admin_group_leave_warning".localized() : "CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE".localized() @@ -576,64 +578,83 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve .filter(id: threadViewModel.threadId) .deleteAll(db) } + completionHandler(true) }) alert.addAction(UIAlertAction( title: "TXT_CANCEL_TITLE".localized(), style: .default - )) + ) { _ in + completionHandler(false) + }) self?.present(alert, animated: true, completion: nil) } delete.themeBackgroundColor = .conversationButton_swipeDestructive - let pin: UITableViewRowAction = UITableViewRowAction( + let pin: UIContextualAction = UIContextualAction( style: .normal, title: (threadViewModel.threadIsPinned ? "UNPIN_BUTTON_TEXT".localized() : "PIN_BUTTON_TEXT".localized() ) - ) { _, _ in - Storage.shared.writeAsync { db in - try SessionThread - .filter(id: threadViewModel.threadId) - .updateAll(db, SessionThread.Columns.isPinned.set(to: !threadViewModel.threadIsPinned)) + ) { _, _, completionHandler in + (tableView.cellForRow(at: indexPath) as? FullConversationCell)?.optimisticUpdate( + isPinned: !threadViewModel.threadIsPinned + ) + completionHandler(true) + + // Delay the change to give the cell "unswipe" animation some time to complete + DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) { + Storage.shared.writeAsync { db in + try SessionThread + .filter(id: threadViewModel.threadId) + .updateAll(db, SessionThread.Columns.isPinned.set(to: !threadViewModel.threadIsPinned)) + } } } pin.themeBackgroundColor = .conversationButton_swipeTertiary guard threadViewModel.threadVariant == .contact && !threadViewModel.threadIsNoteToSelf else { - return [ delete, pin ] + return UISwipeActionsConfiguration(actions: [ delete, pin ]) } - let block: UITableViewRowAction = UITableViewRowAction( + let block: UIContextualAction = UIContextualAction( style: .normal, title: (threadViewModel.threadIsBlocked == true ? "BLOCK_LIST_UNBLOCK_BUTTON".localized() : "BLOCK_LIST_BLOCK_BUTTON".localized() ) - ) { _, _ in - Storage.shared.writeAsync { db in - try Contact - .filter(id: threadViewModel.threadId) - .updateAll( - db, - Contact.Columns.isBlocked.set( - to: (threadViewModel.threadIsBlocked == false ? - true: - false + ) { _, _, completionHandler in + (tableView.cellForRow(at: indexPath) as? FullConversationCell)?.optimisticUpdate( + isBlocked: (threadViewModel.threadIsBlocked == false) + ) + completionHandler(true) + + // Delay the change to give the cell "unswipe" animation some time to complete + DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) { + Storage.shared.writeAsync { db in + try Contact + .filter(id: threadViewModel.threadId) + .updateAll( + db, + Contact.Columns.isBlocked.set( + to: (threadViewModel.threadIsBlocked == false ? + true: + false + ) ) ) - ) - - try MessageSender.syncConfiguration(db, forceSyncNow: true) - .retainUntilComplete() + + try MessageSender.syncConfiguration(db, forceSyncNow: true) + .retainUntilComplete() + } } } block.themeBackgroundColor = .conversationButton_swipeSecondary - return [ delete, block, pin ] + return UISwipeActionsConfiguration(actions: [ delete, block, pin ]) - default: return [] + default: return nil } } diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 3b20abebe..f214d5e15 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -136,6 +136,16 @@ public class HomeViewModel { return SQL("LEFT JOIN \(typingIndicator[.threadId]) = \(thread[.id])") }() + ), + PagedData.ObservedChanges( + table: Setting.self, + columns: [.value], + joinToPagedType: { + let setting: TypedTableAlias = TypedTableAlias() + let targetSetting: String = Setting.BoolKey.showScreenshotNotifications.rawValue + + return SQL("LEFT JOIN \(Setting.self) ON \(setting[.key]) = \(targetSetting)") + }() ) ], /// **Note:** This `optimisedJoinSQL` value includes the required minimum joins needed for the query but differs diff --git a/Session/Home/Message Requests/MessageRequestsViewController.swift b/Session/Home/Message Requests/MessageRequestsViewController.swift index a859cdbcc..810bb3e17 100644 --- a/Session/Home/Message Requests/MessageRequestsViewController.swift +++ b/Session/Home/Message Requests/MessageRequestsViewController.swift @@ -70,7 +70,7 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat }() private lazy var clearAllButton: OutlineButton = { - let result: OutlineButton = OutlineButton(style: .destructive, size: .medium) + let result: OutlineButton = OutlineButton(style: .destructive, size: .large) result.translatesAutoresizingMaskIntoConstraints = false result.setTitle("MESSAGE_REQUESTS_CLEAR_ALL".localized(), for: .normal) result.addTarget(self, action: #selector(clearAllTapped), for: .touchUpInside) @@ -340,24 +340,25 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return true } - - func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { + + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let section: MessageRequestsViewModel.SectionModel = self.viewModel.threadData[indexPath.section] switch section.model { case .threads: let threadId: String = section.elements[indexPath.row].threadId - let delete = UITableViewRowAction( + let delete = UIContextualAction( style: .destructive, title: "TXT_DELETE_TITLE".localized() - ) { [weak self] _, _ in + ) { [weak self] _, _, completionHandler in self?.delete(threadId) + completionHandler(true) } delete.themeBackgroundColor = .conversationButton_swipeDestructive - return [ delete ] + return UISwipeActionsConfiguration(actions: [ delete ]) - default: return [] + default: return nil } } diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 6aa5574ce..057c16478 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -290,7 +290,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD preferredStyle: .alert ) alert.addAction(UIAlertAction(title: "modal_share_logs_title".localized(), style: .default) { _ in - ShareLogsModal.shareLogs(from: alert) { [weak self] in + HelpViewModel.shareLogs(viewControllerToDismiss: alert) { [weak self] in self?.showFailedMigrationAlert(error: error) } }) diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index c00bba4c7..b9f6583e3 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "Benachrichtigungen"; "vc_privacy_settings_title" = "Datenschutz"; "preferences_notifications_strategy_category_title" = "Benachrichtigungsstrategie"; -"modal_seed_title" = "Ihr Wiederherstellungssatz"; -"modal_seed_explanation" = "Das ist Ihr Wiederherstellungssatz. Damit können Sie Ihre Session ID wiederherstellen oder auf ein neues Gerät migrieren."; "vc_qr_code_title" = "QR-Code"; "vc_qr_code_view_my_qr_code_tab_title" = "Meinen QR-Code anzeigen"; "vc_qr_code_view_scan_qr_code_tab_title" = "QR-Code scannen"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Daten nicht gelöscht von Service Node 1. Service Node ID: %@."; "dialog_clear_all_data_deletion_failed_2" = "Daten nicht gelöscht von %@ Service Noten. Service Noten IDs: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Ihr Wiederherstellungssatz"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 4e2c17355..65a9d4f97 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "Notifications"; "vc_privacy_settings_title" = "Privacy"; "preferences_notifications_strategy_category_title" = "Notification Strategy"; -"modal_seed_title" = "Your Recovery Phrase"; -"modal_seed_explanation" = "This is your recovery phrase. With it, you can restore or migrate your Session ID to a new device."; "vc_qr_code_title" = "QR Code"; "vc_qr_code_view_my_qr_code_tab_title" = "View My QR Code"; "vc_qr_code_view_scan_qr_code_tab_title" = "Scan QR Code"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Data not deleted by 1 Service Node. Service Node ID: %@."; "dialog_clear_all_data_deletion_failed_2" = "Data not deleted by %@ Service Nodes. Service Node IDs: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Your Recovery Phrase"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index c6c1c529d..f9c6b0577 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "Notificaciones"; "vc_privacy_settings_title" = "Privacidad"; "preferences_notifications_strategy_category_title" = "Estrategia de notificación"; -"modal_seed_title" = "Tu frase de recuperación"; -"modal_seed_explanation" = "Esta es tu frase de recuperación. Con ella, puedes restaurar o migrar tu ID de Session a un nuevo dispositivo."; "vc_qr_code_title" = "Código QR"; "vc_qr_code_view_my_qr_code_tab_title" = "Ver mi código QR"; "vc_qr_code_view_scan_qr_code_tab_title" = "Escanear código QR"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Datos no borrados por 1 nodo de servicio. ID del nodo de servicio: %@."; "dialog_clear_all_data_deletion_failed_2" = "Datos no borrados por %@ nodos de servicio. ID del nodo de servicio: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Tu frase de recuperación"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index e47cf01e9..21ec8c2de 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "اعلان‌ها"; "vc_privacy_settings_title" = "حریم خصوصی"; "preferences_notifications_strategy_category_title" = "استراتژی اعلان"; -"modal_seed_title" = "عبارت بازیابی شما"; -"modal_seed_explanation" = "این عبارت بازیابی شماست. با استفاده از آن می‌توانید شناسه‌ی Session خود را به دستگاه جدید بازیابی یا انتقال دهید."; "vc_qr_code_title" = "کد QR"; "vc_qr_code_view_my_qr_code_tab_title" = "مشاهده کد QR من"; "vc_qr_code_view_scan_qr_code_tab_title" = "اسکن کد QR"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "داده ها توسط ۱ گره سرویس حذف نشده است. شناسه گره سرویس: %@."; "dialog_clear_all_data_deletion_failed_2" = "داده ها توسط گره سرویس %@ حذف نشدند. شناسه گره سرویس: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "عبارت بازیابی شما"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index 525fcbc85..cae5e433c 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "Ilmoitukset"; "vc_privacy_settings_title" = "Yksityisyys"; "preferences_notifications_strategy_category_title" = "Ilmoitustyyli"; -"modal_seed_title" = "Palatusvirkkeesi"; -"modal_seed_explanation" = "Tämä on palautusvirkkeesi. Sillä voit palauttaa tai siirtää Session ID:si uuteen laitteeseen."; "vc_qr_code_title" = "QR-koodi"; "vc_qr_code_view_my_qr_code_tab_title" = "Näytä QR-koodini"; "vc_qr_code_view_scan_qr_code_tab_title" = "Skannaa QR-koodi"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Dataa ei poistettu yhdestä palvelusolmusta. Palvelusolmun tunnus: %@."; "dialog_clear_all_data_deletion_failed_2" = "Dataa ei poistettu %@ palvelusolmusta. Palvelusolmujen tunnukset: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Palatusvirkkeesi"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 8574ef5d9..dfdfa8a5c 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "Notifications"; "vc_privacy_settings_title" = "Confidientalité"; "preferences_notifications_strategy_category_title" = "Stratégie de notification"; -"modal_seed_title" = "Votre phrase de récupération"; -"modal_seed_explanation" = "Ceci est votre phrase de récupération. Elle vous permet de restaurer ou migrer votre Session ID vers un nouvel appareil."; "vc_qr_code_title" = "Code QR"; "vc_qr_code_view_my_qr_code_tab_title" = "Afficher mon code QR"; "vc_qr_code_view_scan_qr_code_tab_title" = "Scanner le QR Code"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Les données n’ont pas été supprimées sur un nœud de service. ID du nœud de service : %@."; "dialog_clear_all_data_deletion_failed_2" = "Les données n’ont pas été supprimées sur %@ nœuds de service. ID des nœuds de service : %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Votre phrase de récupération"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index 34a2e4ce9..e9da8e268 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "Notifications"; "vc_privacy_settings_title" = "Privacy"; "preferences_notifications_strategy_category_title" = "Notification Strategy"; -"modal_seed_title" = "Your Recovery Phrase"; -"modal_seed_explanation" = "This is your recovery phrase. With it, you can restore or migrate your Session ID to a new device."; "vc_qr_code_title" = "QR Code"; "vc_qr_code_view_my_qr_code_tab_title" = "View My QR Code"; "vc_qr_code_view_scan_qr_code_tab_title" = "Scan QR Code"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Data not deleted by 1 Service Node. Service Node ID: %@."; "dialog_clear_all_data_deletion_failed_2" = "Data not deleted by %@ Service Nodes. Service Node IDs: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Your Recovery Phrase"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index 7214f4024..a35f6405e 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "Obavijesti"; "vc_privacy_settings_title" = "Privatnost"; "preferences_notifications_strategy_category_title" = "Strategija obavijesti"; -"modal_seed_title" = "Fraza za oporavak"; -"modal_seed_explanation" = "Ovo je vaša fraza za oporavak. Pomoću nje možete vratiti ili migrirati svoj Session ID na novi uređaj."; "vc_qr_code_title" = "QR kôd"; "vc_qr_code_view_my_qr_code_tab_title" = "Pogledaj moj QR kôd"; "vc_qr_code_view_scan_qr_code_tab_title" = "Skeniraj QR kôd"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Podatke nije izbrisao 1 uslužni čvor. ID uslužnog čvora: %@."; "dialog_clear_all_data_deletion_failed_2" = "Podatke nije izbrisao %@ uslužni čvor. ID uslužnog čvora: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Fraza za oporavak"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 7d2a5368d..8b03a2d01 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "Notifikasi"; "vc_privacy_settings_title" = "Privasi"; "preferences_notifications_strategy_category_title" = "Strategi notofikasi"; -"modal_seed_title" = "Kata pemulihan anda"; -"modal_seed_explanation" = "Ini adalah kata pemulihan anda. Gunakan untuk mengembalikan atau memindahkan Session ID anda ke perangkat lain"; "vc_qr_code_title" = "Kode QR"; "vc_qr_code_view_my_qr_code_tab_title" = "Lihat kode QR saya"; "vc_qr_code_view_scan_qr_code_tab_title" = "Pindai kode QR"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Data tidak dihapus oleh 1 Service Node. Service Node ID: %@."; "dialog_clear_all_data_deletion_failed_2" = "Data tidak dihapus oleh %@ Service Nodes. Service Node IDs: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Kata pemulihan anda"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index 029b96cf7..127932a90 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "Notifiche"; "vc_privacy_settings_title" = "Privacy"; "preferences_notifications_strategy_category_title" = "Strategia di notifica"; -"modal_seed_title" = "Frase di recupero"; -"modal_seed_explanation" = "Questa è la tua frase di recupero. Usala per ripristinare o migrare la Sessione ID a un nuovo dispositivo."; "vc_qr_code_title" = "Codice QR"; "vc_qr_code_view_my_qr_code_tab_title" = "Visualizza il mio codice QR"; "vc_qr_code_view_scan_qr_code_tab_title" = "Scansiona il codice QR"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Dati non eliminati da 1 nodi di servizio. ID Nodo di servizio: %@."; "dialog_clear_all_data_deletion_failed_2" = "Dati non eliminati da %@ nodi di servizio. ID Nodo di servizio: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Frase di recupero"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index 0828d7abb..41c67b9b0 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "通知"; "vc_privacy_settings_title" = "プライバシー"; "preferences_notifications_strategy_category_title" = "通知戦略"; -"modal_seed_title" = "あなたのリカバリーフレーズ"; -"modal_seed_explanation" = "これはあなたのリカバリーフレーズです。これにより、Session ID を新しい端末に復元または移行できます。"; "vc_qr_code_title" = "QR コード"; "vc_qr_code_view_my_qr_code_tab_title" = "私の QR コードを表示する"; "vc_qr_code_view_scan_qr_code_tab_title" = "QR コードをスキャンする"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "このサービスノードからデータが削除されませんでした。ID: %@"; "dialog_clear_all_data_deletion_failed_2" = "%@ つのサービスノードからデータが削除されませんでした。ID %@"; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "あなたのリカバリーフレーズ"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index 5ecb8fc7c..8c0590d9e 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "Meldingen"; "vc_privacy_settings_title" = "Privacy"; "preferences_notifications_strategy_category_title" = "Notificatie Inhoud"; -"modal_seed_title" = "Uw Herstel Zin"; -"modal_seed_explanation" = "Dit is uw herstel zin, Hiermee kun je je sessie-ID herstellen of migreren naar een nieuw apparaat."; "vc_qr_code_title" = "QR-code"; "vc_qr_code_view_my_qr_code_tab_title" = "Bekijk mijn QR-code"; "vc_qr_code_view_scan_qr_code_tab_title" = "QR-code scannen"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Gegevens niet verwijderd door 1 Service Node. Service Node ID: %@."; "dialog_clear_all_data_deletion_failed_2" = "Gegevens niet verwijderd door %@ Service Nodes. Service Node IDs: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Uw Herstel Zin"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index f68ae17bb..430babaad 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "Powiadomienia"; "vc_privacy_settings_title" = "Prywatność"; "preferences_notifications_strategy_category_title" = "Strategia powiadomień"; -"modal_seed_title" = "Twoja fraza odzyskiwania"; -"modal_seed_explanation" = "To jest twoja fraza odzyskiwania. Dzięki niej możesz przywrócić lub przenieść identyfikator Session na nowe urządzenie."; "vc_qr_code_title" = "Kod QR"; "vc_qr_code_view_my_qr_code_tab_title" = "Wyświetl mój kod QR"; "vc_qr_code_view_scan_qr_code_tab_title" = "Skanowania QR code"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Dane nie zostały usunięte przez 1 węzeł usługowy. Identyfikator węzła: %@."; "dialog_clear_all_data_deletion_failed_2" = "Dane nie zostały usunięte przez %@ węzły usługowe. Identyfikatory węzłów: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Twoja fraza odzyskiwania"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index d35320674..a27f93a0f 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "Notificações"; "vc_privacy_settings_title" = "Privacidade"; "preferences_notifications_strategy_category_title" = "Estratégia de notificação"; -"modal_seed_title" = "Sua frase de recuperação"; -"modal_seed_explanation" = "Esta é sua frase de recuperação. Com ela, você pode restaurar ou migrar seu ID Session para um novo dispositivo."; "vc_qr_code_title" = "Código QR"; "vc_qr_code_view_my_qr_code_tab_title" = "Ver meu código QR"; "vc_qr_code_view_scan_qr_code_tab_title" = "Escanear código QR"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Dados não excluídos por 1 Service Node. ID do Service Node: %@."; "dialog_clear_all_data_deletion_failed_2" = "Dados não excluídos por %@ Service Nodes. IDs dos Service Nodes: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Sua frase de recuperação"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index 61c48305e..ba72fb614 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "Уведомления"; "vc_privacy_settings_title" = "Конфиденциальность"; "preferences_notifications_strategy_category_title" = "Метод уведомлений"; -"modal_seed_title" = "Ваша секретная фраза"; -"modal_seed_explanation" = "Это ваша секретная фраза. С ее помощью вы можете восстановить или перенести свой Session ID на новое устройство."; "vc_qr_code_title" = "QR-код"; "vc_qr_code_view_my_qr_code_tab_title" = "Посмотреть мой QR-код"; "vc_qr_code_view_scan_qr_code_tab_title" = "Сканировать QR-код"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Данные не удалены 1 узлом сервиса. Номер узла: %@."; "dialog_clear_all_data_deletion_failed_2" = "Данные не удалены %@ узлами сервиса. Номера узлов: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Ваша секретная фраза"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index cff40c1f2..38907b476 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "දැනුම්දීම්"; "vc_privacy_settings_title" = "පෞද්ගලිකත්වය"; "preferences_notifications_strategy_category_title" = "Notification Strategy"; -"modal_seed_title" = "Your Recovery Phrase"; -"modal_seed_explanation" = "This is your recovery phrase. With it, you can restore or migrate your Session ID to a new device."; "vc_qr_code_title" = "QR Code"; "vc_qr_code_view_my_qr_code_tab_title" = "View My QR Code"; "vc_qr_code_view_scan_qr_code_tab_title" = "Scan QR Code"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Data not deleted by 1 Service Node. Service Node ID: %@."; "dialog_clear_all_data_deletion_failed_2" = "Data not deleted by %@ Service Nodes. Service Node IDs: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Your Recovery Phrase"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index c45b95219..735e9b319 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "Hlásenia"; "vc_privacy_settings_title" = "Súkromie"; "preferences_notifications_strategy_category_title" = "Stratégia upozornení"; -"modal_seed_title" = "Vaša fráza pre obnovenie"; -"modal_seed_explanation" = "Toto je vaša fráza pre obnovenie. S jej pomocou môžete obnoviť alebo presunúť svoje Session ID na nové zariadenie."; "vc_qr_code_title" = "QR kód"; "vc_qr_code_view_my_qr_code_tab_title" = "Zobraziť môj QR kód"; "vc_qr_code_view_scan_qr_code_tab_title" = "Skenovať QR kód"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Data neboli odstránené 1 Servisným Uzlom. Servisný Uzol ID:%@."; "dialog_clear_all_data_deletion_failed_2" = "Data neboli odstránené 1 Servisným Uzlom. Servisný Uzol ID:%@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Vaša fráza pre obnovenie"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index 859cea734..063c1f903 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "Aviseringar"; "vc_privacy_settings_title" = "Integritet"; "preferences_notifications_strategy_category_title" = "Strategi för aviseringar"; -"modal_seed_title" = "Din Återställningsfras"; -"modal_seed_explanation" = "Detta är din återställningsfras. Med den kan du återställa eller migrera ditt Sessions-ID till en ny enhet."; "vc_qr_code_title" = "QR-kod"; "vc_qr_code_view_my_qr_code_tab_title" = "Visa min QR-kod"; "vc_qr_code_view_scan_qr_code_tab_title" = "Skanna QR-kod"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Data not deleted by 1 Service Node. Service Node ID: %@."; "dialog_clear_all_data_deletion_failed_2" = "Data not deleted by %@ Service Nodes. Service Node IDs: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Din Återställningsfras"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index 95075b99d..101948b47 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "การแจ้งเตือน"; "vc_privacy_settings_title" = "ความเป็นส่วนตัว"; "preferences_notifications_strategy_category_title" = "กลยุทธ์สำคัญแจ้งเตือน"; -"modal_seed_title" = "วลีกู้คืนของคุณ"; -"modal_seed_explanation" = "นี่คือวลีกู้คืนของคุณ ด้วยวิธีนี้ คุณสามารถกู้คืนหรือย้ายไอดีเซสชันSessionของคุณไปยังอุปกรณ์ใหม่ได้"; "vc_qr_code_title" = "QR โค้ด"; "vc_qr_code_view_my_qr_code_tab_title" = "แสดง QR โค้ดของคุน"; "vc_qr_code_view_scan_qr_code_tab_title" = "สแกน QR โค้ด"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Data not deleted by 1 Service Node. Service Node ID: %@."; "dialog_clear_all_data_deletion_failed_2" = "Data not deleted by %@ Service Nodes. Service Node IDs: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "วลีกู้คืนของคุณ"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index 72379b97a..0225096db 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "Thông báo"; "vc_privacy_settings_title" = "Riêng tưy"; "preferences_notifications_strategy_category_title" = "Chiến lược thông báo"; -"modal_seed_title" = "Cụm từ khôi phục của bạn"; -"modal_seed_explanation" = "Đây là cụm từ khôi phục của bạn. Bạn có thể dùng nó để khôi phục hoặc chuyển Session ID của mình sang một thiết bị mới."; "vc_qr_code_title" = "Mã QR"; "vc_qr_code_view_my_qr_code_tab_title" = "Xem mã QR của tôi"; "vc_qr_code_view_scan_qr_code_tab_title" = "Quét mã QR"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "Data not deleted by 1 Service Node. Service Node ID: %@."; "dialog_clear_all_data_deletion_failed_2" = "Data not deleted by %@ Service Nodes. Service Node IDs: %@."; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "Cụm từ khôi phục của bạn"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index 7c8abca3b..6dc2845d2 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "通知"; "vc_privacy_settings_title" = "隱私權條款"; "preferences_notifications_strategy_category_title" = "通知類型"; -"modal_seed_title" = "您的回復用字句"; -"modal_seed_explanation" = "這是您的回復用字句,您可以利用此字句來回復或轉移您的帳號至新的裝置上。"; "vc_qr_code_title" = "QR Code"; "vc_qr_code_view_my_qr_code_tab_title" = "查看我的 QR Code"; "vc_qr_code_view_scan_qr_code_tab_title" = "掃描 QR Code"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "數據已被1個服務節點刪除。節點ID: %@"; "dialog_clear_all_data_deletion_failed_2" = "數據沒有被%@個服務節點刪除。節點ID: %@"; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "您的回復用字句"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 1858bee8f..1f4f33435 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -520,8 +520,6 @@ "vc_notification_settings_title" = "通知"; "vc_privacy_settings_title" = "隐私"; "preferences_notifications_strategy_category_title" = "通知选项"; -"modal_seed_title" = "您的恢复口令"; -"modal_seed_explanation" = "这是您的恢复口令。您可以通过该口令将Session ID还原或迁移到新设备上。"; "vc_qr_code_title" = "二维码"; "vc_qr_code_view_my_qr_code_tab_title" = "查看我的二维码"; "vc_qr_code_view_scan_qr_code_tab_title" = "扫描二维码"; @@ -701,7 +699,7 @@ "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION" = "Require Touch ID, Face ID or your passcode to unlock Session."; "PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_TITLE" = "Screenshot Notifications"; -"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a direct message."; +"PRIVACY_SCREEN_SECURITY_SCREENSHOT_NOTIFICATIONS_DESCRIPTION" = "Receive a notification when a contact takes a screenshot of a one-to-one chat."; "PRIVACY_SECTION_READ_RECEIPTS" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_TITLE" = "Read Receipts"; "PRIVACY_READ_RECEIPTS_DESCRIPTION" = "See and share read receipts in one-to-one chats."; @@ -730,6 +728,18 @@ "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_AND_CONTENT" = "Name and Content"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NAME_ONLY" = "Name Only"; "NOTIFICATIONS_STYLE_CONTENT_OPTION_NO_NAME_OR_CONTENT" = "No Name or Content"; +"CONVERSATION_SETTINGS_TITLE" = "Conversations"; +"CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING" = "Message Trimming"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE" = "Trim Communities"; +"CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION" = "Delete messages older than 6 months from Communities that have over 2,000 messages."; +"CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES" = "Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE" = "Autoplay Audio Messages"; +"CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION" = "Autoplay consecutive audio messages."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE" = "Blocked Contacts"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE" = "You have no blocked contacts."; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK" = "Unblock"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE" = "Are you sure you want to unblock these contacts?"; +"CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON" = "Unblock"; "APPEARANCE_TITLE" = "Appearance"; "APPEARANCE_THEMES_TITLE" = "Themes"; "APPEARANCE_PRIMARY_COLOR_TITLE" = "Primary colour"; @@ -754,3 +764,5 @@ "dialog_clear_all_data_deletion_failed_1" = "数据未被一个服务节点删除。服务节点ID: %@"; "dialog_clear_all_data_deletion_failed_2" = "数据未被 %@ 服务节点删除。服务节点ID: %@"; "modal_clear_all_data_confirm" = "Clear"; +"modal_seed_title" = "您的恢复口令"; +"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; diff --git a/Session/Path/PathVC.swift b/Session/Path/PathVC.swift index 4cab218ca..e2fd13de0 100644 --- a/Session/Path/PathVC.swift +++ b/Session/Path/PathVC.swift @@ -15,6 +15,7 @@ final class PathVC: BaseVC { private lazy var pathStackView: UIStackView = { let result = UIStackView() result.axis = .vertical + return result }() @@ -22,6 +23,7 @@ final class PathVC: BaseVC { let result = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil) result.set(.width, to: 64) result.set(.height, to: 64) + return result }() @@ -29,6 +31,7 @@ final class PathVC: BaseVC { let result = OutlineButton(style: .regular, size: .large) result.setTitle("vc_path_learn_more_button_title".localized(), for: UIControl.State.normal) result.addTarget(self, action: #selector(learnMore), for: UIControl.Event.touchUpInside) + return result }() @@ -50,11 +53,12 @@ final class PathVC: BaseVC { // Set up explanation label let explanationLabel = UILabel() explanationLabel.font = .systemFont(ofSize: Values.smallFontSize) - explanationLabel.textColor = Colors.text.withAlphaComponent(Values.mediumOpacity) explanationLabel.text = "vc_path_explanation".localized() - explanationLabel.numberOfLines = 0 + explanationLabel.themeTextColor = .textSecondary explanationLabel.textAlignment = .center explanationLabel.lineBreakMode = .byWordWrapping + explanationLabel.numberOfLines = 0 + // Set up path stack view let pathStackViewContainer = UIView() pathStackViewContainer.addSubview(pathStackView) @@ -68,12 +72,15 @@ final class PathVC: BaseVC { pathStackViewContainer.trailingAnchor.constraint(greaterThanOrEqualTo: spinner.trailingAnchor).isActive = true pathStackViewContainer.bottomAnchor.constraint(greaterThanOrEqualTo: spinner.bottomAnchor).isActive = true spinner.center(in: pathStackViewContainer) + // Set up rebuild path button let inset: CGFloat = isIPhone5OrSmaller ? 64 : 80 let learnMoreButtonContainer = UIView(wrapping: learnMoreButton, withInsets: UIEdgeInsets(top: 0, leading: inset, bottom: 0, trailing: inset), shouldAdaptForIPadWithWidth: Values.iPadButtonWidth) + // Set up spacers let topSpacer = UIView.vStretchingSpacer() let bottomSpacer = UIView.vStretchingSpacer() + // Set up main stack view let mainStackView = UIStackView(arrangedSubviews: [ explanationLabel, topSpacer, pathStackViewContainer, bottomSpacer, learnMoreButtonContainer ]) mainStackView.axis = .vertical @@ -82,8 +89,10 @@ final class PathVC: BaseVC { mainStackView.isLayoutMarginsRelativeArrangement = true view.addSubview(mainStackView) mainStackView.pin(to: view) + // Set up spacer constraints topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor).isActive = true + // Perform initial update update() } @@ -99,7 +108,8 @@ final class PathVC: BaseVC { NotificationCenter.default.removeObserver(self) } - // MARK: Updating + // MARK: - Updating + @objc private func handleBuildingPathsNotification() { update() } @objc private func handlePathsBuiltNotification() { update() } @objc private func handleOnionRequestPathCountriesLoadedNotification() { update() } @@ -130,14 +140,14 @@ final class PathVC: BaseVC { } let youRow = getPathRow( - title: NSLocalizedString("vc_path_device_row_title", comment: ""), + title: "vc_path_device_row_title".localized(), subtitle: nil, location: .top, dotAnimationStartDelay: 1, dotAnimationRepeatInterval: dotAnimationRepeatInterval ) let destinationRow = getPathRow( - title: NSLocalizedString("vc_path_destination_row_title", comment: ""), + title: "vc_path_destination_row_title".localized(), subtitle: nil, location: .bottom, dotAnimationStartDelay: Double(pathToDisplay.count) + 2, @@ -152,7 +162,8 @@ final class PathVC: BaseVC { } } - // MARK: General + // MARK: - General + private func getPathRow(title: String, subtitle: String?, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double) -> UIStackView { let lineView = LineView( location: location, @@ -194,7 +205,8 @@ final class PathVC: BaseVC { return getPathRow(title: title, subtitle: country, location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval) } - // MARK: Interaction + // MARK: - Interaction + @objc private func learnMore() { let urlAsString = "https://getsession.org/faq/#onion-routing" let url = URL(string: urlAsString)! @@ -202,8 +214,9 @@ final class PathVC: BaseVC { } } -// MARK: Line View -private final class LineView : UIView { +// MARK: - Line View + +private final class LineView: UIView { private let location: Location private let dotAnimationStartDelay: Double private let dotAnimationRepeatInterval: Double @@ -217,12 +230,22 @@ private final class LineView : UIView { private lazy var dotView: UIView = { let result = UIView() - result.layer.cornerRadius = PathVC.dotSize / 2 - let glowRadius: CGFloat = isLightMode ? 1 : 2 - let glowColor = isLightMode ? UIColor.black.withAlphaComponent(0.4) : UIColor.black - let glowConfiguration = UIView.CircularGlowConfiguration(size: PathVC.dotSize, color: glowColor, isAnimated: true, animationDuration: 0.5, radius: glowRadius) - result.setCircularGlow(with: glowConfiguration) - result.backgroundColor = Colors.accent + result.themeBackgroundColor = .path_connected + result.layer.themeShadowColor = .path_connected + result.layer.shadowOffset = .zero + result.layer.shadowPath = UIBezierPath( + ovalIn: CGRect( + origin: CGPoint.zero, + size: CGSize(width: PathVC.dotSize, height: PathVC.dotSize) + ) + ).cgPath + result.layer.cornerRadius = (PathVC.dotSize / 2) + + ThemeManager.onThemeChange(observer: result) { [weak result] theme, _ in + result?.layer.shadowOpacity = (theme.interfaceStyle == .light ? 0.4 : 1) + result?.layer.shadowRadius = (theme.interfaceStyle == .light ? 1 : 2) + } + return result }() @@ -247,28 +270,33 @@ private final class LineView : UIView { private func setUpViewHierarchy() { let lineView = UIView() lineView.set(.width, to: Values.separatorThickness) - lineView.backgroundColor = Colors.text + lineView.themeBackgroundColor = .textPrimary addSubview(lineView) + lineView.center(.horizontal, in: self) + switch location { - case .top: lineView.topAnchor.constraint(equalTo: centerYAnchor).isActive = true - case .middle, .bottom: lineView.pin(.top, to: .top, of: self) + case .top: lineView.topAnchor.constraint(equalTo: centerYAnchor).isActive = true + case .middle, .bottom: lineView.pin(.top, to: .top, of: self) } + switch location { - case .top, .middle: lineView.pin(.bottom, to: .bottom, of: self) - case .bottom: lineView.bottomAnchor.constraint(equalTo: centerYAnchor).isActive = true + case .top, .middle: lineView.pin(.bottom, to: .bottom, of: self) + case .bottom: lineView.bottomAnchor.constraint(equalTo: centerYAnchor).isActive = true } + let dotSize = PathVC.dotSize dotViewWidthConstraint = dotView.set(.width, to: dotSize) dotViewHeightConstraint = dotView.set(.height, to: dotSize) addSubview(dotView) + dotView.center(in: self) + + let repeatInterval: TimeInterval = self.dotAnimationRepeatInterval Timer.scheduledTimer(withTimeInterval: dotAnimationStartDelay, repeats: false) { [weak self] _ in - guard let strongSelf = self else { return } - strongSelf.animate() - strongSelf.dotViewAnimationTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.dotAnimationRepeatInterval, repeats: true) { _ in - guard let strongSelf = self else { return } - strongSelf.animate() + self?.animate() + self?.dotViewAnimationTimer = Timer.scheduledTimer(withTimeInterval: repeatInterval, repeats: true) { _ in + self?.animate() } } } @@ -279,36 +307,21 @@ private final class LineView : UIView { private func animate() { expandDot() + Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] _ in self?.collapseDot() } } private func expandDot() { - let newSize = PathVC.expandedDotSize - let newGlowRadius: CGFloat = isLightMode ? 4 : 6 - let newGlowColor = Colors.accent.withAlphaComponent(0.6) - updateDotView(size: newSize, glowRadius: newGlowRadius, glowColor: newGlowColor) + UIView.animate(withDuration: 0.5) { [weak self] in + self?.dotView.transform = CGAffineTransform.scale(PathVC.expandedDotSize / PathVC.dotSize) + } } private func collapseDot() { - let newSize = PathVC.dotSize - let newGlowRadius: CGFloat = isLightMode ? 1 : 2 - let newGlowColor = isLightMode ? UIColor.black.withAlphaComponent(0.4) : UIColor.black - updateDotView(size: newSize, glowRadius: newGlowRadius, glowColor: newGlowColor) - } - - private func updateDotView(size: CGFloat, glowRadius: CGFloat, glowColor: UIColor) { - let frame = CGRect(center: dotView.center, size: CGSize(width: size, height: size)) - dotViewWidthConstraint.constant = size - dotViewHeightConstraint.constant = size - UIView.animate(withDuration: 0.5) { - self.layoutIfNeeded() - self.dotView.frame = frame - self.dotView.layer.cornerRadius = size / 2 - let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: glowColor, isAnimated: true, animationDuration: 0.5, radius: glowRadius) - self.dotView.setCircularGlow(with: glowConfiguration) - self.dotView.backgroundColor = Colors.accent + UIView.animate(withDuration: 0.5) { [weak self] in + self?.dotView.transform = CGAffineTransform.scale(1) } } } diff --git a/Session/Settings/BlockedContactsViewController.swift b/Session/Settings/BlockedContactsViewController.swift new file mode 100644 index 000000000..452a1e2bc --- /dev/null +++ b/Session/Settings/BlockedContactsViewController.swift @@ -0,0 +1,382 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import GRDB +import DifferenceKit +import SessionUIKit +import SessionMessagingKit +import SignalUtilitiesKit + +class BlockedContactsViewController: BaseVC, UITableViewDelegate, UITableViewDataSource { + private static let loadingHeaderHeight: CGFloat = 20 + + private let viewModel: BlockedContactsViewModel = BlockedContactsViewModel() + private var dataChangeObservable: DatabaseCancellable? + private var hasLoadedInitialContactData: Bool = false + private var isLoadingMore: Bool = false + private var isAutoLoadingNextPage: Bool = false + private var viewHasAppeared: Bool = false + + // MARK: - Intialization + + init() { + Storage.shared.addObserver(viewModel.pagedDataObserver) + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + preconditionFailure("Use init() instead.") + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: - UI + + private lazy var tableView: UITableView = { + let result: UITableView = UITableView() + result.translatesAutoresizingMaskIntoConstraints = false + result.backgroundColor = .clear + result.separatorStyle = .none + result.register(view: BlockedContactCell.self) + result.dataSource = self + result.delegate = self + + let bottomInset = Values.newConversationButtonBottomOffset + NewConversationButtonSet.expandedButtonSize + Values.largeSpacing + NewConversationButtonSet.collapsedButtonSize + result.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0) + result.showsVerticalScrollIndicator = false + + if #available(iOS 15.0, *) { + result.sectionHeaderTopPadding = 0 + } + + return result + }() + + private lazy var emptyStateLabel: UILabel = { + let result: UILabel = UILabel() + result.translatesAutoresizingMaskIntoConstraints = false + result.isUserInteractionEnabled = false + result.font = UIFont.systemFont(ofSize: Values.smallFontSize) + result.text = NSLocalizedString("CONVERSATION_SETTINGS_BLOCKED_CONTACTS_EMPTY_STATE", comment: "") + result.textColor = Colors.text + result.textAlignment = .center + result.numberOfLines = 0 + result.isHidden = true + + return result + }() + + private lazy var unblockButton: OutlineButton = { + let result: OutlineButton = OutlineButton(style: .destructive, size: .large) + result.translatesAutoresizingMaskIntoConstraints = false + result.setTitle("CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK".localized(), for: .normal) + result.addTarget(self, action: #selector(unblockTapped), for: .touchUpInside) + + return result + }() + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + ViewControllerUtilities.setUpDefaultSessionStyle( + for: self, + title: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE".localized(), + hasCustomBackButton: false + ) + + // Add the UI (MUST be done after the thread freeze so the 'tableView' creation and setting + // the dataSource has the correct data) + view.addSubview(tableView) + view.addSubview(emptyStateLabel) + view.addSubview(unblockButton) + setupLayout() + + // Notifications + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidBecomeActive(_:)), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidResignActive(_:)), + name: UIApplication.didEnterBackgroundNotification, object: nil + ) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + startObservingChanges() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.viewHasAppeared = true + self.autoLoadNextPageIfNeeded() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + // Stop observing database changes + dataChangeObservable?.cancel() + } + + @objc func applicationDidBecomeActive(_ notification: Notification) { + startObservingChanges(didReturnFromBackground: true) + } + + @objc func applicationDidResignActive(_ notification: Notification) { + // Stop observing database changes + dataChangeObservable?.cancel() + } + + // MARK: - Layout + + private func setupLayout() { + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.smallSpacing), + tableView.leftAnchor.constraint(equalTo: view.leftAnchor), + tableView.rightAnchor.constraint(equalTo: view.rightAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + emptyStateLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: Values.massiveSpacing), + emptyStateLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: Values.mediumSpacing), + emptyStateLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -Values.mediumSpacing), + emptyStateLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + + unblockButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + unblockButton.bottomAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.bottomAnchor, + constant: -Values.largeSpacing + ), + unblockButton.widthAnchor.constraint(equalToConstant: Values.iPadButtonWidth), + unblockButton.heightAnchor.constraint(equalToConstant: NewConversationButtonSet.collapsedButtonSize) + ]) + } + + // MARK: - Updating + + private func startObservingChanges(didReturnFromBackground: Bool = false) { + self.viewModel.onContactChange = { [weak self] updatedContactData in + self?.handleContactUpdates(updatedContactData) + } + + // Note: When returning from the background we could have received notifications but the + // PagedDatabaseObserver won't have them so we need to force a re-fetch of the current + // data to ensure everything is up to date + if didReturnFromBackground { + self.viewModel.pagedDataObserver?.reload() + } + } + + private func handleContactUpdates(_ updatedData: [BlockedContactsViewModel.SectionModel], initialLoad: Bool = false) { + // Ensure the first load runs without animations (if we don't do this the cells will animate + // in from a frame of CGRect.zero) + guard hasLoadedInitialContactData else { + hasLoadedInitialContactData = true + UIView.performWithoutAnimation { handleContactUpdates(updatedData, initialLoad: true) } + return + } + + // Show the empty state if there is no data + unblockButton.isEnabled = !viewModel.selectedContactIds.isEmpty + unblockButton.isHidden = updatedData.isEmpty + emptyStateLabel.isHidden = !updatedData.isEmpty + + CATransaction.begin() + CATransaction.setCompletionBlock { [weak self] in + // Complete page loading + self?.isLoadingMore = false + self?.autoLoadNextPageIfNeeded() + } + + // Reload the table content (animate changes after the first load) + tableView.reload( + using: StagedChangeset(source: viewModel.contactData, target: updatedData), + deleteSectionsAnimation: .none, + insertSectionsAnimation: .none, + reloadSectionsAnimation: .none, + deleteRowsAnimation: .bottom, + insertRowsAnimation: .top, + reloadRowsAnimation: .none, + interrupt: { $0.changeCount > 100 } // Prevent too many changes from causing performance issues + ) { [weak self] updatedData in + self?.viewModel.updateContactData(updatedData) + } + + CATransaction.commit() + } + + private func autoLoadNextPageIfNeeded() { + guard !self.isAutoLoadingNextPage && !self.isLoadingMore else { return } + + self.isAutoLoadingNextPage = true + + DispatchQueue.main.asyncAfter(deadline: .now() + PagedData.autoLoadNextPageDelay) { [weak self] in + self?.isAutoLoadingNextPage = false + + // Note: We sort the headers as we want to prioritise loading newer pages over older ones + let sections: [(BlockedContactsViewModel.Section, CGRect)] = (self?.viewModel.contactData + .enumerated() + .map { index, section in + (section.model, (self?.tableView.rectForHeader(inSection: index) ?? .zero)) + }) + .defaulting(to: []) + let shouldLoadMore: Bool = sections + .contains { section, headerRect in + section == .loadMore && + headerRect != .zero && + (self?.tableView.bounds.contains(headerRect) == true) + } + + guard shouldLoadMore else { return } + + self?.isLoadingMore = true + + DispatchQueue.global(qos: .default).async { [weak self] in + self?.viewModel.pagedDataObserver?.load(.pageAfter) + } + } + } + + // MARK: - UITableViewDataSource + + func numberOfSections(in tableView: UITableView) -> Int { + return viewModel.contactData.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let section: BlockedContactsViewModel.SectionModel = viewModel.contactData[section] + + return section.elements.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let section: BlockedContactsViewModel.SectionModel = viewModel.contactData[indexPath.section] + + switch section.model { + case .contacts: + let cellViewModel: BlockedContactsViewModel.DataModel = section.elements[indexPath.row] + let cell: BlockedContactCell = tableView.dequeue(type: BlockedContactCell.self, for: indexPath) + cell.update( + with: cellViewModel, + isSelected: viewModel.selectedContactIds.contains(cellViewModel.id) + ) + + return cell + + default: preconditionFailure("Other sections should have no content") + } + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let section: BlockedContactsViewModel.SectionModel = viewModel.contactData[section] + + switch section.model { + case .loadMore: + let loadingIndicator: UIActivityIndicatorView = UIActivityIndicatorView(style: .medium) + loadingIndicator.tintColor = Colors.text + loadingIndicator.alpha = 0.5 + loadingIndicator.startAnimating() + + let view: UIView = UIView() + view.addSubview(loadingIndicator) + loadingIndicator.center(in: view) + + return view + + default: return nil + } + } + + // MARK: - UITableViewDelegate + + func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { + return UITableView.automaticDimension + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return UITableView.automaticDimension + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + let section: BlockedContactsViewModel.SectionModel = viewModel.contactData[section] + + switch section.model { + case .loadMore: return BlockedContactsViewController.loadingHeaderHeight + default: return 0 + } + } + + func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { + guard self.hasLoadedInitialContactData && self.viewHasAppeared && !self.isLoadingMore else { return } + + let section: BlockedContactsViewModel.SectionModel = self.viewModel.contactData[section] + + switch section.model { + case .loadMore: + self.isLoadingMore = true + + DispatchQueue.global(qos: .default).async { [weak self] in + self?.viewModel.pagedDataObserver?.load(.pageAfter) + } + + default: break + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let section: BlockedContactsViewModel.SectionModel = self.viewModel.contactData[indexPath.section] + + switch section.model { + case .contacts: + let cellViewModel: BlockedContactsViewModel.DataModel = section.elements[indexPath.row] + + self.viewModel.toggleSelection(contactId: cellViewModel.id) + self.tableView.reloadRows(at: [indexPath], with: .none) + self.unblockButton.isEnabled = !self.viewModel.selectedContactIds.isEmpty + + default: break + } + } + + // MARK: - Interaction + + @objc private func unblockTapped() { + guard !viewModel.selectedContactIds.isEmpty else { return } + + let contactIds: Set = viewModel.selectedContactIds + let confirmationModal: ConfirmationModal = ConfirmationModal( + info: ConfirmationModal.Info( + title: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_TITLE".localized(), + confirmTitle: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_UNBLOCK_CONFIRMATION_ACTON".localized(), + confirmStyle: .danger, + cancelStyle: .textPrimary + ) + ) { [weak self] _ in + // Unblock the contacts + Storage.shared.write { db in + _ = try Contact + .filter(ids: contactIds) + .updateAll(db, Contact.Columns.isBlocked.set(to: false)) + + // Force a config sync + try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete() + } + + self?.dismiss(animated: true, completion: nil) + } + self.present(confirmationModal, animated: true, completion: nil) + } +} diff --git a/Session/Settings/BlockedContactsViewModel.swift b/Session/Settings/BlockedContactsViewModel.swift new file mode 100644 index 000000000..f14b024b4 --- /dev/null +++ b/Session/Settings/BlockedContactsViewModel.swift @@ -0,0 +1,214 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import DifferenceKit +import SignalUtilitiesKit + +public class BlockedContactsViewModel { + public typealias SectionModel = ArraySection + + // MARK: - Section + + public enum Section: Differentiable { + case contacts + case loadMore + } + + // MARK: - Variables + + public static let pageSize: Int = 30 + + // MARK: - Initialization + + init() { + self.pagedDataObserver = nil + + // Note: Since this references self we need to finish initializing before setting it, we + // also want to skip the initial query and trigger it async so that the push animation + // doesn't stutter (it should load basically immediately but without this there is a + // distinct stutter) + self.pagedDataObserver = PagedDatabaseObserver( + pagedTable: Profile.self, + pageSize: BlockedContactsViewModel.pageSize, + idColumn: .id, + observedChanges: [ + PagedData.ObservedChanges( + table: Profile.self, + columns: [ + .id, + .name, + .nickname, + .profilePictureFileName + ] + ), + PagedData.ObservedChanges( + table: Contact.self, + columns: [.isBlocked], + joinToPagedType: { + let profile: TypedTableAlias = TypedTableAlias() + let contact: TypedTableAlias = TypedTableAlias() + + return SQL("JOIN \(Contact.self) ON \(contact[.id]) = \(profile[.id])") + }() + ) + ], + /// **Note:** This `optimisedJoinSQL` value includes the required minimum joins needed for the query + joinSQL: DataModel.optimisedJoinSQL, + filterSQL: DataModel.filterSQL, + orderSQL: DataModel.orderSQL, + dataQuery: DataModel.query( + filterSQL: DataModel.filterSQL, + orderSQL: DataModel.orderSQL + ), + onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in + guard let updatedContactData: [SectionModel] = self?.process(data: updatedData, for: updatedPageInfo) else { + return + } + + // If we have the 'onThreadChange' callback then trigger it, otherwise just store the changes + // to be sent to the callback if we ever start observing again (when we have the callback it needs + // to do the data updating as it's tied to UI updates and can cause crashes if not updated in the + // correct order) + guard let onContactChange: (([SectionModel]) -> ()) = self?.onContactChange else { + self?.unobservedContactDataChanges = updatedContactData + return + } + + onContactChange(updatedContactData) + } + ) + + // Run the initial query on a background thread so we don't block the push transition + DispatchQueue.global(qos: .default).async { [weak self] in + // The `.pageBefore` will query from a `0` offset loading the first page + self?.pagedDataObserver?.load(.pageBefore) + } + } + + // MARK: - Contact Data + + public private(set) var selectedContactIds: Set = [] + public private(set) var unobservedContactDataChanges: [SectionModel]? + public private(set) var contactData: [SectionModel] = [] + public private(set) var pagedDataObserver: PagedDatabaseObserver? + + public var onContactChange: (([SectionModel]) -> ())? { + didSet { + // When starting to observe interaction changes we want to trigger a UI update just in case the + // data was changed while we weren't observing + if let unobservedContactDataChanges: [SectionModel] = self.unobservedContactDataChanges { + onContactChange?(unobservedContactDataChanges) + self.unobservedContactDataChanges = nil + } + } + } + + private func process(data: [DataModel], for pageInfo: PagedData.PageInfo) -> [SectionModel] { + // Update the 'selectedContactIds' to only include selected contacts which are within the + // data (ie. handle profile deletions) + let profileIds: Set = data.map { $0.id }.asSet() + selectedContactIds = selectedContactIds.intersection(profileIds) + + return [ + [ + SectionModel( + section: .contacts, + elements: data + .sorted { lhs, rhs -> Bool in + lhs.profile.displayName() > rhs.profile.displayName() + } + ) + ], + (!data.isEmpty && (pageInfo.pageOffset + pageInfo.currentCount) < pageInfo.totalCount ? + [SectionModel(section: .loadMore)] : + [] + ) + ].flatMap { $0 } + } + + public func updateContactData(_ updatedData: [SectionModel]) { + self.contactData = updatedData + } + + public func toggleSelection(contactId: String) { + guard selectedContactIds.contains(contactId) else { + selectedContactIds.insert(contactId) + return + } + + selectedContactIds.remove(contactId) + } + + // MARK: - DataModel + + public struct DataModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable { + public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) + public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue) + + public static let profileString: String = CodingKeys.profile.stringValue + + public var differenceIdentifier: String { profile.id } + public var id: String { profile.id } + + public let rowId: Int64 + public let profile: Profile + + static func query( + filterSQL: SQL, + orderSQL: SQL + ) -> (([Int64]) -> AdaptedFetchRequest>) { + return { rowIds -> AdaptedFetchRequest> in + let profile: TypedTableAlias = TypedTableAlias() + + /// **Note:** The `numColumnsBeforeProfile` value **MUST** match the number of fields before + /// the `DataModel.profileKey` entry below otherwise the query will fail to + /// parse and might throw + /// + /// Explicitly set default values for the fields ignored for search results + let numColumnsBeforeProfile: Int = 1 + + let request: SQLRequest = """ + SELECT + \(profile.alias[Column.rowID]) AS \(DataModel.rowIdKey), + \(DataModel.profileKey).* + + FROM \(Profile.self) + WHERE \(profile.alias[Column.rowID]) IN \(rowIds) + ORDER BY \(orderSQL) + """ + + return request.adapted { db in + let adapters = try splittingRowAdapters(columnCounts: [ + numColumnsBeforeProfile, + Profile.numberOfSelectedColumns(db) + ]) + + return ScopeAdapter([ + DataModel.profileString: adapters[1] + ]) + } + } + } + + static var optimisedJoinSQL: SQL = { + let profile: TypedTableAlias = TypedTableAlias() + let contact: TypedTableAlias = TypedTableAlias() + + return SQL("JOIN \(Contact.self) ON \(contact[.id]) = \(profile[.id])") + }() + + static var filterSQL: SQL = { + let contact: TypedTableAlias = TypedTableAlias() + + return SQL("\(contact[.isBlocked]) = true") + }() + + static let orderSQL: SQL = { + let profile: TypedTableAlias = TypedTableAlias() + + return SQL("IFNULL(IFNULL(\(profile[.nickname]), \(profile[.name])), \(profile[.id])) ASC") + }() + } + +} diff --git a/Session/Settings/ConversationSettingsViewController.swift b/Session/Settings/ConversationSettingsViewController.swift deleted file mode 100644 index 971e47850..000000000 --- a/Session/Settings/ConversationSettingsViewController.swift +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import UIKit -import SessionUIKit -import SignalUtilitiesKit - -// FIXME: Refactor to be MVVM and use database observation -class ConversationSettingsViewController: OWSTableViewController { - // MARK: - Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - - self.updateTableContents() - - ViewControllerUtilities.setUpDefaultSessionStyle(for: self, title: "CONVERSATIONS_TITLE".localized(), hasCustomBackButton: false) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - self.updateTableContents() - } - - // MARK: - Table Contents - - func updateTableContents() { - let updatedContents: OWSTableContents = OWSTableContents() - - let messageTrimming: OWSTableSection = OWSTableSection() - messageTrimming.headerTitle = "MESSAGE_TRIMMING_TITLE".localized() - messageTrimming.footerTitle = "MESSAGE_TRIMMING_OPEN_GROUP_DESCRIPTION".localized() - messageTrimming.add(OWSTableItem.switch( - withText: "MESSAGE_TRIMMING_OPEN_GROUP_TITLE".localized(), - isOn: { Storage.shared[.trimOpenGroupMessagesOlderThanSixMonths] }, - target: self, - selector: #selector(didToggleTrimOpenGroupsSwitch(_:)) - )) - updatedContents.addSection(messageTrimming) - - self.contents = updatedContents - } - - // MARK: - Actions - - @objc private func didToggleTrimOpenGroupsSwitch(_ sender: UISwitch) { - let switchIsOn: Bool = sender.isOn - - Storage.shared.writeAsync( - updates: { db in - db[.trimOpenGroupMessagesOlderThanSixMonths] = !switchIsOn - }, - completion: { [weak self] _, _ in - self?.updateTableContents() - } - ) - } - - @objc private func close(_ sender: UIBarButtonItem) { - self.navigationController?.dismiss(animated: true) - } -} diff --git a/Session/Settings/ConversationSettingsViewModel.swift b/Session/Settings/ConversationSettingsViewModel.swift new file mode 100644 index 000000000..90600115b --- /dev/null +++ b/Session/Settings/ConversationSettingsViewModel.swift @@ -0,0 +1,91 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import DifferenceKit +import SessionUIKit +import SessionMessagingKit +import SessionUtilitiesKit + +class ConversationSettingsViewModel: SettingsTableViewModel { + // MARK: - Section + + public enum Section: SettingSection { + case messageTrimming + case audioMessages + case blockedContacts + + var title: String { + switch self { + case .messageTrimming: return "CONVERSATION_SETTINGS_SECTION_MESSAGE_TRIMMING".localized() + case .audioMessages: return "CONVERSATION_SETTINGS_SECTION_AUDIO_MESSAGES".localized() + case .blockedContacts: return "" // No title + } + } + } + + // MARK: - Content + + override var title: String { "CONVERSATION_SETTINGS_TITLE".localized() } + + private var _settingsData: [SectionModel] = [] + public override var settingsData: [SectionModel] { _settingsData } + + public override var observableSettingsData: ObservableData { _observableSettingsData } + + /// This is all the data the screen needs to populate itself, please see the following link for tips to help optimise + /// performance https://github.com/groue/GRDB.swift#valueobservation-performance + /// + /// **Note:** This observation will be triggered twice immediately (and be de-duped by the `removeDuplicates`) + /// this is due to the behaviour of `ValueConcurrentObserver.asyncStartObservation` which triggers it's own + /// fetch (after the ones in `ValueConcurrentObserver.asyncStart`/`ValueConcurrentObserver.syncStart`) + /// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this + private lazy var _observableSettingsData: ObservableData = ValueObservation + .trackingConstantRegion { db -> [SectionModel] in + return [ + SectionModel( + model: .messageTrimming, + elements: [ + SettingInfo( + id: .messageTrimming, + title: "CONVERSATION_SETTINGS_MESSAGE_TRIMMING_TITLE".localized(), + subtitle: "CONVERSATION_SETTINGS_MESSAGE_TRIMMING_DESCRIPTION".localized(), + action: .settingBool(key: .trimOpenGroupMessagesOlderThanSixMonths) + ) + ] + ), + SectionModel( + model: .audioMessages, + elements: [ + SettingInfo( + id: .audioMessages, + title: "CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_TITLE".localized(), + subtitle: "CONVERSATION_SETTINGS_AUDIO_MESSAGES_AUTOPLAY_DESCRIPTION".localized(), + action: .settingBool(key: .shouldAutoPlayConsecutiveAudioMessages) + ) + ] + ), + SectionModel( + model: .blockedContacts, + elements: [ + SettingInfo( + id: .blockedContacts, + title: "CONVERSATION_SETTINGS_BLOCKED_CONTACTS_TITLE".localized(), + action: .dangerPush(createDestination: { + BlockedContactsViewController() + }) + ) + ] + ) + ] + } + .removeDuplicates() + + // MARK: - Functions + + public override func updateSettings(_ updatedSettings: [SectionModel]) { + self._settingsData = updatedSettings + } + + public override func saveChanges() {} +} diff --git a/Session/Settings/HelpViewModel.swift b/Session/Settings/HelpViewModel.swift index 4c8e53521..1e31747cc 100644 --- a/Session/Settings/HelpViewModel.swift +++ b/Session/Settings/HelpViewModel.swift @@ -46,15 +46,9 @@ class HelpViewModel: SettingsTableViewModel ())? = nil + ) { + let version: String = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) + .defaulting(to: "") + OWSLogger.info("[Version] iOS \(UIDevice.current.systemVersion) \(version)") + DDLog.flushLog() + + let logFilePaths: [String] = AppEnvironment.shared.fileLogger.logFileManager.sortedLogFilePaths + + guard + let latestLogFilePath: String = logFilePaths.first, + let viewController: UIViewController = CurrentAppContext().frontmostViewController() + else { return } + + let showShareSheet: () -> () = { + let shareVC = UIActivityViewController( + activityItems: [ URL(fileURLWithPath: latestLogFilePath) ], + applicationActivities: nil + ) + shareVC.completionWithItemsHandler = { _, _, _, _ in onShareComplete?() } + + if UIDevice.current.isIPad { + shareVC.excludedActivityTypes = [] + shareVC.popoverPresentationController?.permittedArrowDirections = (targetView != nil ? [.up] : []) + shareVC.popoverPresentationController?.sourceView = (targetView ?? viewController.view) + shareVC.popoverPresentationController?.sourceRect = (targetView ?? viewController.view).bounds + } + viewController.present(shareVC, animated: true, completion: nil) + } + + guard let viewControllerToDismiss: UIViewController = viewControllerToDismiss else { + showShareSheet() + return + } + + viewControllerToDismiss.dismiss(animated: true) { + showShareSheet() + } + } } diff --git a/Session/Settings/NotificationSoundViewModel.swift b/Session/Settings/NotificationSoundViewModel.swift index df663ea71..b3ba512fd 100644 --- a/Session/Settings/NotificationSoundViewModel.swift +++ b/Session/Settings/NotificationSoundViewModel.swift @@ -8,9 +8,17 @@ import SessionMessagingKit import SessionUtilitiesKit class NotificationSoundViewModel: SettingsTableViewModel { + // FIXME: Remove `threadId` once we ditch the per-thread notification sound + private let threadId: String? private var audioPlayer: OWSAudioPlayer? private var currentSelection: Preferences.Sound? + // MARK: - Initialization + + init(threadId: String? = nil) { + self.threadId = threadId + } + deinit { self.audioPlayer?.stop() self.audioPlayer = nil @@ -106,3 +114,14 @@ class NotificationSoundViewModel: SettingsTableViewModel UIViewController { + return SettingsTableViewController( + viewModel: NotificationSoundViewModel(threadId: threadId) + ) + } +} diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index 06c753c19..0f26d2f4f 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -7,7 +7,6 @@ import SessionMessagingKit import SignalUtilitiesKit final class NukeDataModal: Modal { - // MARK: - Components private lazy var titleLabel: UILabel = { diff --git a/Session/Settings/PrivacySettingsViewModel.swift b/Session/Settings/PrivacySettingsViewModel.swift index f0bfeac76..7d30831b2 100644 --- a/Session/Settings/PrivacySettingsViewModel.swift +++ b/Session/Settings/PrivacySettingsViewModel.swift @@ -89,6 +89,32 @@ class PrivacySettingsViewModel: SettingsTableViewModel UIView? { let section: SectionModel = viewModel.settingsData[section] let view: SettingHeaderView = tableView.dequeueHeaderFooterView(type: SettingHeaderView.self) - view.update(with: section.model.title) + view.update( + with: section.model.title, + hasSeparator: (section.elements.first?.action.shouldHaveBackground != false) + ) return view } // MARK: - UITableViewDelegate + + func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { + return UITableView.automaticDimension + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return UITableView.automaticDimension + } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) @@ -245,9 +257,12 @@ class SettingsTableViewController: Equatable, Hashable, Differen let title: String let subtitle: String? let action: SettingsAction + let subtitleExtraViewGenerator: (() -> UIView)? let extraActionTitle: ((Theme, Theme.PrimaryColor) -> NSAttributedString)? let onExtraAction: (() -> Void)? @@ -47,6 +48,7 @@ struct SettingInfo: Equatable, Hashable, Differen id: ID, title: String, subtitle: String? = nil, + subtitleExtraViewGenerator: (() -> UIView)? = nil, action: SettingsAction, extraActionTitle: ((Theme, Theme.PrimaryColor) -> NSAttributedString)? = nil, onExtraAction: (() -> Void)? = nil @@ -54,6 +56,7 @@ struct SettingInfo: Equatable, Hashable, Differen self.id = id self.title = title self.subtitle = subtitle + self.subtitleExtraViewGenerator = subtitleExtraViewGenerator self.action = action self.extraActionTitle = extraActionTitle self.onExtraAction = onExtraAction @@ -107,9 +110,9 @@ public enum SettingsAction: Hashable, Equatable { shouldAutoSave: Bool, selectValue: () -> Void ) - case rightButtonModal( + case rightButtonAction( title: String, - createModal: () -> UIViewController + action: (UIView) -> () ) private var actionName: String { @@ -122,7 +125,14 @@ public enum SettingsAction: Hashable, Equatable { case .push: return "push" case .dangerPush: return "dangerPush" case .listSelection: return "listSelection" - case .rightButtonModal: return "rightButtonModal" + case .rightButtonAction: return "rightButtonAction" + } + } + + var shouldHaveBackground: Bool { + switch self { + case .dangerPush: return false + default: return true } } diff --git a/Session/Settings/SettingsVC.swift b/Session/Settings/SettingsVC.swift index b17b26b28..c0a97ef6b 100644 --- a/Session/Settings/SettingsVC.swift +++ b/Session/Settings/SettingsVC.swift @@ -254,7 +254,7 @@ final class SettingsVC: BaseVC, AvatarViewHelperDelegate { UIView.separator(), getSettingButton(title: "vc_settings_notifications_button_title".localized(), action: #selector(showNotificationSettings)), UIView.separator(), - getSettingButton(title: "CONVERSATIONS_TITLE".localized(), action: #selector(showConversationSettings)), + getSettingButton(title: "CONVERSATION_SETTINGS_TITLE".localized(), action: #selector(showConversationSettings)), UIView.separator(), getSettingButton(title: "MESSAGE_REQUESTS_TITLE".localized(), action: #selector(showMessageRequests)), UIView.separator(), @@ -539,8 +539,10 @@ final class SettingsVC: BaseVC, AvatarViewHelperDelegate { } @objc private func showPrivacySettings() { - let privacySettingsVC = PrivacySettingsTableViewController() - self.navigationController?.pushViewController(privacySettingsVC, animated: true) + let settingsViewController: SettingsTableViewController = SettingsTableViewController( + viewModel: PrivacySettingsViewModel() + ) + self.navigationController?.pushViewController(settingsViewController, animated: true) } @objc private func showNotificationSettings() { @@ -556,8 +558,10 @@ final class SettingsVC: BaseVC, AvatarViewHelperDelegate { } @objc private func showConversationSettings() { - let conversationSettingsVC = ConversationSettingsViewController() - self.navigationController?.pushViewController(conversationSettingsVC, animated: true) + let settingsViewController: SettingsTableViewController = SettingsTableViewController( + viewModel: ConversationSettingsViewModel() + ) + self.navigationController?.pushViewController(settingsViewController, animated: true) } @objc private func showAppearanceSettings() { @@ -597,26 +601,4 @@ final class SettingsVC: BaseVC, AvatarViewHelperDelegate { } navigationController!.present(shareVC, animated: true, completion: nil) } - - @objc private func openFAQ() { - let url = URL(string: "https://getsession.org/faq")! - UIApplication.shared.open(url) - } - - @objc private func openSurvey() { - let url = URL(string: "https://getsession.org/survey")! - UIApplication.shared.open(url) - } - - @objc private func shareLogs() { - let shareLogsModal = ShareLogsModal() - shareLogsModal.modalPresentationStyle = .overFullScreen - shareLogsModal.modalTransitionStyle = .crossDissolve - present(shareLogsModal, animated: true, completion: nil) - } - - @objc private func helpTranslate() { - let url = URL(string: "https://crowdin.com/project/session-ios")! - UIApplication.shared.open(url) - } } diff --git a/Session/Settings/ShareLogsModal.swift b/Session/Settings/ShareLogsModal.swift deleted file mode 100644 index cd4723d05..000000000 --- a/Session/Settings/ShareLogsModal.swift +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import UIKit -import SignalUtilitiesKit - -final class ShareLogsModal: Modal { - - // MARK: - Lifecycle - - init() { - super.init(nibName: nil, bundle: nil) - } - - override init(nibName: String?, bundle: Bundle?) { - preconditionFailure("Use init() instead.") - } - - required init?(coder: NSCoder) { - preconditionFailure("Use init() instead.") - } - - override func populateContentView() { - // Title - let titleLabel = UILabel() - titleLabel.textColor = Colors.text - titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize) - titleLabel.text = NSLocalizedString("modal_share_logs_title", comment: "") - titleLabel.textAlignment = .center - - // Message - let messageLabel = UILabel() - messageLabel.textColor = Colors.text.withAlphaComponent(Values.mediumOpacity) - messageLabel.font = .systemFont(ofSize: Values.smallFontSize) - messageLabel.text = NSLocalizedString("modal_share_logs_explanation", comment: "") - messageLabel.numberOfLines = 0 - messageLabel.lineBreakMode = .byWordWrapping - messageLabel.textAlignment = .center - - // Open button - let shareButton = UIButton() - shareButton.set(.height, to: Values.mediumButtonHeight) - shareButton.layer.cornerRadius = Modal.buttonCornerRadius - shareButton.backgroundColor = Colors.buttonBackground - shareButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize) - shareButton.setTitleColor(Colors.text, for: UIControl.State.normal) - shareButton.setTitle(NSLocalizedString("share", comment: ""), for: UIControl.State.normal) - shareButton.addTarget(self, action: #selector(shareLogs), for: UIControl.Event.touchUpInside) - - // Button stack view - let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, shareButton ]) - buttonStackView.axis = .horizontal - buttonStackView.spacing = Values.mediumSpacing - buttonStackView.distribution = .fillEqually - - // Content stack view - let contentStackView = UIStackView(arrangedSubviews: [ titleLabel, messageLabel ]) - contentStackView.axis = .vertical - contentStackView.spacing = Values.largeSpacing - - // Main stack view - let spacing = Values.largeSpacing - Values.smallFontSize / 2 - let mainStackView = UIStackView(arrangedSubviews: [ contentStackView, buttonStackView ]) - mainStackView.axis = .vertical - mainStackView.spacing = spacing - contentView.addSubview(mainStackView) - mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing) - mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing) - contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.largeSpacing) - contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: spacing) - } - - // MARK: - Interaction - - @objc private func shareLogs() { - ShareLogsModal.shareLogs(from: self) - } - - public static func shareLogs(from viewController: UIViewController, onShareComplete: (() -> ())? = nil) { - let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" - OWSLogger.info("[Version] iOS \(UIDevice.current.systemVersion) \(version)") - DDLog.flushLog() - - let logFilePaths = AppEnvironment.shared.fileLogger.logFileManager.sortedLogFilePaths - if let latestLogFilePath = logFilePaths.first { - let latestLogFileURL = URL(fileURLWithPath: latestLogFilePath) - - viewController.dismiss(animated: true, completion: { - if let vc = CurrentAppContext().frontmostViewController() { - let shareVC = UIActivityViewController(activityItems: [ latestLogFileURL ], applicationActivities: nil) - shareVC.completionWithItemsHandler = { _, _, _, _ in onShareComplete?() } - - if UIDevice.current.isIPad { - shareVC.excludedActivityTypes = [] - shareVC.popoverPresentationController?.permittedArrowDirections = [] - shareVC.popoverPresentationController?.sourceView = vc.view - shareVC.popoverPresentationController?.sourceRect = vc.view.bounds - } - vc.present(shareVC, animated: true, completion: nil) - } - }) - } - } -} diff --git a/Session/Settings/Views/BlockedContactCell.swift b/Session/Settings/Views/BlockedContactCell.swift new file mode 100644 index 000000000..be232a3fc --- /dev/null +++ b/Session/Settings/Views/BlockedContactCell.swift @@ -0,0 +1,90 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SessionUIKit +import SessionMessagingKit +import SignalUtilitiesKit + +class BlockedContactCell: UITableViewCell { + // MARK: - Components + + private lazy var profilePictureView: ProfilePictureView = ProfilePictureView() + + private let selectionView: RadioButton = { + let result: RadioButton = RadioButton(size: .medium) + result.translatesAutoresizingMaskIntoConstraints = false + result.isUserInteractionEnabled = false + result.font = .systemFont(ofSize: Values.mediumFontSize, weight: .bold) + + return result + }() + + // MARK: - Initializtion + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + setUpViewHierarchy() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + + setUpViewHierarchy() + } + + // MARK: - Layout + + private func setUpViewHierarchy() { + // Background color + themeBackgroundColor = .conversationButton_background + + // Highlight color + let selectedBackgroundView = UIView() + selectedBackgroundView.themeBackgroundColor = .conversationButton_highlight + self.selectedBackgroundView = selectedBackgroundView + + // Add the UI + contentView.addSubview(profilePictureView) + contentView.addSubview(selectionView) + + setupLayout() + } + + private func setupLayout() { + // Profile picture view + profilePictureView.center(.vertical, in: contentView) + profilePictureView.topAnchor + .constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: Values.mediumSpacing) + .isActive = true + profilePictureView.bottomAnchor + .constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -Values.mediumSpacing) + .isActive = true + profilePictureView.pin(.left, to: .left, of: contentView, withInset: Values.veryLargeSpacing) + profilePictureView.set(.width, to: Values.mediumProfilePictureSize) + profilePictureView.set(.height, to: Values.mediumProfilePictureSize) + profilePictureView.size = Values.mediumProfilePictureSize + + selectionView.center(.vertical, in: contentView) + selectionView.topAnchor + .constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: Values.mediumSpacing) + .isActive = true + selectionView.bottomAnchor + .constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -Values.mediumSpacing) + .isActive = true + selectionView.pin(.left, to: .right, of: profilePictureView, withInset: Values.mediumSpacing) + selectionView.pin(.right, to: .right, of: contentView, withInset: -Values.veryLargeSpacing) + } + + // MARK: - Content + + public func update(with cellViewModel: BlockedContactsViewModel.DataModel, isSelected: Bool) { + profilePictureView.update( + publicKey: cellViewModel.profile.id, + profile: cellViewModel.profile, + threadVariant: .contact + ) + selectionView.text = cellViewModel.profile.displayName() + selectionView.update(isSelected: isSelected) + } +} diff --git a/Session/Settings/Views/SettingsCell.swift b/Session/Settings/Views/SettingsCell.swift index f97ff0cae..dd5b874e7 100644 --- a/Session/Settings/Views/SettingsCell.swift +++ b/Session/Settings/Views/SettingsCell.swift @@ -7,6 +7,7 @@ import SessionUIKit class SettingsCell: UITableViewCell { /// This value is here to allow the theming update callback to be released when preparing for reuse private var instanceView: UIView = UIView() + private var subtitleExtraView: UIView? private var onExtraAction: (() -> Void)? // MARK: - UI @@ -140,7 +141,7 @@ class SettingsCell: UITableViewCell { return result }() - private lazy var rightActionButtonContainerView: UIView = { + public lazy var rightActionButtonContainerView: UIView = { let result: UIView = UIView() result.translatesAutoresizingMaskIntoConstraints = false result.themeBackgroundColor = .solidButton_background @@ -182,8 +183,6 @@ class SettingsCell: UITableViewCell { } private func setupViewHierarchy() { - themeBackgroundColor = .settings_tabBackground - // Highlight color let selectedBackgroundView = UIView() selectedBackgroundView.themeBackgroundColor = .settings_tabHighlight @@ -259,6 +258,8 @@ class SettingsCell: UITableViewCell { override func prepareForReuse() { super.prepareForReuse() + self.themeBackgroundColor = nil + self.selectedBackgroundView = nil self.instanceView = UIView() self.onExtraAction = nil @@ -279,11 +280,15 @@ class SettingsCell: UITableViewCell { tickImageView.alpha = 1 rightActionButtonContainerView.isHidden = true botSeparator.isHidden = true + + subtitleExtraView?.removeFromSuperview() + subtitleExtraView = nil } public func update( title: String, subtitle: String?, + subtitleExtraViewGenerator: (() -> UIView)?, action: SettingsAction, extraActionTitle: ((Theme, Theme.PrimaryColor) -> NSAttributedString)?, onExtraAction: (() -> Void)?, @@ -291,6 +296,7 @@ class SettingsCell: UITableViewCell { isLastInSection: Bool ) { self.instanceView = UIView() + self.subtitleExtraView = subtitleExtraViewGenerator?() self.onExtraAction = onExtraAction // Left content @@ -299,15 +305,66 @@ class SettingsCell: UITableViewCell { subtitleLabel.isHidden = (subtitle == nil) extraActionButton.isHidden = (extraActionTitle == nil) - // Separator Visibility - switch action { - case .dangerPush: - topSeparator.isHidden = true - botSeparator.isHidden = true - - default: - topSeparator.isHidden = isFirstInSection - botSeparator.isHidden = !isLastInSection + // Position the 'subtitleExtraView' at the end of the last line of text + if + let subtitleExtraView: UIView = self.subtitleExtraView, + let subtitle: String = subtitle, + let font: UIFont = subtitleLabel.font + { + self.layoutIfNeeded() + + let layoutManager: NSLayoutManager = NSLayoutManager() + let textStorage = NSTextStorage( + attributedString: NSAttributedString( + string: subtitle, + attributes: [ .font: font ] + ) + ) + textStorage.addLayoutManager(layoutManager) + + let textContainer: NSTextContainer = NSTextContainer( + size: CGSize( + width: subtitleLabel.bounds.size.width, + height: 999 + ) + ) + textContainer.lineFragmentPadding = 0 + layoutManager.addTextContainer(textContainer) + + var glyphRange: NSRange = NSRange() + layoutManager.characterRange( + forGlyphRange: NSRange(location: subtitle.glyphCount - 1, length: 1), + actualGlyphRange: &glyphRange + ) + let lastGlyphRect: CGRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) + contentView.addSubview(subtitleExtraView) + + subtitleExtraView.pin( + .top, + to: .top, + of: subtitleLabel, + withInset: (lastGlyphRect.minY + ((lastGlyphRect.height / 2) - (subtitleExtraView.bounds.height / 2))) + ) + subtitleExtraView.pin( + .left, + to: .left, + of: subtitleLabel, + withInset: lastGlyphRect.minX + 5 + ) + } + + // Separator/background Visibility + if action.shouldHaveBackground { + self.themeBackgroundColor = .settings_tabBackground + + topSeparator.isHidden = isFirstInSection + botSeparator.isHidden = !isLastInSection + } + else { + self.themeBackgroundColor = nil + + topSeparator.isHidden = true + botSeparator.isHidden = true } // Action Behaviours @@ -351,7 +408,7 @@ class SettingsCell: UITableViewCell { titleLabel.themeTextColor = .danger actionContainerView.isHidden = false - case .rightButtonModal(let title, _): + case .rightButtonAction(let title, _): actionContainerView.isHidden = false rightActionButtonContainerView.isHidden = false rightActionButtonLabel.text = title diff --git a/Session/Settings/Views/ThemeSelectionView.swift b/Session/Settings/Views/ThemeSelectionView.swift index cf060bf41..9ced38f13 100644 --- a/Session/Settings/Views/ThemeSelectionView.swift +++ b/Session/Settings/Views/ThemeSelectionView.swift @@ -50,31 +50,11 @@ class ThemeSelectionView: UIView { return result }() - private let titleLabel: UILabel = { - let result: UILabel = UILabel() + private let selectionView: RadioButton = { + let result: RadioButton = RadioButton(size: .medium) result.translatesAutoresizingMaskIntoConstraints = false result.isUserInteractionEnabled = false - result.font = UIFont.systemFont(ofSize: Values.mediumFontSize, weight: .bold) - result.themeTextColor = .textPrimary - - return result - }() - - private let selectionBorderView: UIView = { - let result: UIView = UIView() - result.translatesAutoresizingMaskIntoConstraints = false - result.isUserInteractionEnabled = false - result.layer.borderWidth = 1 - result.layer.cornerRadius = (ThemeSelectionView.selectionBorderSize / 2) - - return result - }() - - private let selectionView: UIView = { - let result: UIView = UIView() - result.translatesAutoresizingMaskIntoConstraints = false - result.isUserInteractionEnabled = false - result.layer.cornerRadius = (ThemeSelectionView.selectionSize / 2) + result.font = .systemFont(ofSize: Values.mediumFontSize, weight: .bold) return result }() @@ -104,13 +84,11 @@ class ThemeSelectionView: UIView { previewView.layer.borderColor = theme.colors[.borderSeparator]?.cgColor previewIncomingMessageView.backgroundColor = theme.colors[.messageBubble_incomingBackground] previewOutgoingMessageView.backgroundColor = theme.colors[.defaultPrimary] - titleLabel.text = theme.title + selectionView.text = theme.title // Add the UI addSubview(backgroundButton) addSubview(previewView) - addSubview(titleLabel) - addSubview(selectionBorderView) addSubview(selectionView) previewView.addSubview(previewIncomingMessageView) @@ -142,30 +120,15 @@ class ThemeSelectionView: UIView { previewOutgoingMessageView.set(.width, to: 40) previewOutgoingMessageView.set(.height, to: 12) - titleLabel.center(.vertical, in: self) - titleLabel.pin(.left, to: .right, of: previewView, withInset: Values.mediumSpacing) - - selectionBorderView.center(.vertical, in: self) - selectionBorderView.pin(.right, to: .right, of: self, withInset: -Values.veryLargeSpacing) - selectionBorderView.set(.width, to: ThemeSelectionView.selectionBorderSize) - selectionBorderView.set(.height, to: ThemeSelectionView.selectionBorderSize) - - selectionView.center(in: selectionBorderView) - selectionView.set(.width, to: ThemeSelectionView.selectionSize) - selectionView.set(.height, to: ThemeSelectionView.selectionSize) + selectionView.center(.vertical, in: self) + selectionView.pin(.left, to: .right, of: previewView, withInset: Values.mediumSpacing) + selectionView.pin(.right, to: .right, of: self, withInset: -Values.veryLargeSpacing) } // MARK: - Content func update(isSelected: Bool) { - selectionBorderView.themeBorderColor = (isSelected ? - .radioButton_selectedBorder : - .radioButton_unselectedBorder - ) - selectionView.themeBackgroundColor = (isSelected ? - .radioButton_selectedBackground : - .radioButton_unselectedBackground - ) + selectionView.update(isSelected: isSelected) } @objc func itemSelected() { diff --git a/Session/Shared/FullConversationCell.swift b/Session/Shared/FullConversationCell.swift index c486242f6..7ca0d70b1 100644 --- a/Session/Shared/FullConversationCell.swift +++ b/Session/Shared/FullConversationCell.swift @@ -414,6 +414,26 @@ public final class FullConversationCell: UITableViewCell { ) } + public func optimisticUpdate( + isBlocked: Bool? = nil, + isPinned: Bool? = nil + ) { + if let isBlocked: Bool = isBlocked { + if isBlocked { + accentLineView.themeBackgroundColor = .danger + accentLineView.alpha = 1 + } + else { + accentLineView.themeBackgroundColor = .conversationButton_unreadStripBackground + accentLineView.alpha = (!unreadCountView.isHidden ? 1 : 0.0001) // Setting the alpha to exactly 0 causes an issue on iOS 12 + } + } + + if let isPinned: Bool = isPinned { + isPinnedIcon.isHidden = !isPinned + } + } + // MARK: - Snippet generation private func getSnippet( diff --git a/Session/Sheets & Modals/Modal.swift b/Session/Sheets & Modals/Modal.swift index 1f56e722f..9111b7e65 100644 --- a/Session/Sheets & Modals/Modal.swift +++ b/Session/Sheets & Modals/Modal.swift @@ -112,7 +112,15 @@ public class Modal: BaseVC, UIGestureRecognizerDelegate { // MARK: - Interaction @objc func close() { - dismiss(animated: true, completion: nil) + // Recursively dismiss all modals (ie. find the first modal presented by a non-modal + // and get that to dismiss it's presented view controller) + var targetViewController: UIViewController? = self + + while targetViewController?.presentingViewController is Modal { + targetViewController = targetViewController?.presentingViewController + } + + targetViewController?.presentingViewController?.dismiss(animated: true, completion: nil) } // MARK: - UIGestureRecognizerDelegate diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index 7032267e0..e0f069a46 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -602,10 +602,26 @@ extension MessageViewModel { public extension MessageViewModel { static func filterSQL(threadId: String) -> SQL { let interaction: TypedTableAlias = TypedTableAlias() + let setting: TypedTableAlias = TypedTableAlias() - return SQL("\(interaction[.threadId]) = \(threadId)") + var targetValue: Bool = true + let boolSettingLiteral: Data = Data(bytes: &targetValue, count: MemoryLayout.size(ofValue: targetValue)) + + return SQL(""" + \(interaction[.threadId]) = \(threadId) AND ( + \(SQL("\(interaction[.variant]) != \(Interaction.Variant.infoScreenshotNotification)")) OR + \(SQL("IFNULL(\(setting[.value]), false) == \(boolSettingLiteral)")) + ) + """) } + static let optimisedJoinSQL: SQL = { + let setting: TypedTableAlias = TypedTableAlias() + let targetSetting: String = Setting.BoolKey.showScreenshotNotifications.rawValue + + return SQL("LEFT JOIN \(Setting.self) ON \(setting[.key]) = \(targetSetting)") + }() + static let groupSQL: SQL = { let interaction: TypedTableAlias = TypedTableAlias() diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 6da07d210..454456724 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -425,6 +425,11 @@ public extension SessionThreadViewModel { let attachment: TypedTableAlias = TypedTableAlias() let interactionAttachment: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() + let setting: TypedTableAlias = TypedTableAlias() + let targetSetting: String = Setting.BoolKey.showScreenshotNotifications.rawValue + + var targetValue: Bool = true + let boolSettingLiteral: Data = Data(bytes: &targetValue, count: MemoryLayout.size(ofValue: targetValue)) let interactionTimestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) let interactionStateInteractionIdColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name) @@ -512,7 +517,12 @@ public extension SessionThreadViewModel { SUM(\(interaction[.wasRead]) = false AND \(interaction[.hasMention]) = true) AS \(ViewModel.threadUnreadMentionCountKey) FROM \(Interaction.self) - WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) + LEFT JOIN \(Setting.self) ON \(setting[.key]) = \(targetSetting) + WHERE + \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) AND ( + \(SQL("\(interaction[.variant]) != \(Interaction.Variant.infoScreenshotNotification)")) OR + \(SQL("IFNULL(\(setting[.value]), false) == \(boolSettingLiteral)")) + ) GROUP BY \(interaction[.threadId]) ) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id]) @@ -611,6 +621,11 @@ public extension SessionThreadViewModel { let thread: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() let interaction: TypedTableAlias = TypedTableAlias() + let setting: TypedTableAlias = TypedTableAlias() + let targetSetting: String = Setting.BoolKey.showScreenshotNotifications.rawValue + + var targetValue: Bool = true + let boolSettingLiteral: Data = Data(bytes: &targetValue, count: MemoryLayout.size(ofValue: targetValue)) let interactionTimestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) @@ -621,7 +636,12 @@ public extension SessionThreadViewModel { \(interaction[.threadId]), MAX(\(interaction[.timestampMs])) AS \(interactionTimestampMsColumnLiteral) FROM \(Interaction.self) - WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) + LEFT JOIN \(Setting.self) ON \(setting[.key]) = \(targetSetting) + WHERE + \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) AND ( + \(SQL("\(interaction[.variant]) != \(Interaction.Variant.infoScreenshotNotification)")) OR + \(SQL("IFNULL(\(setting[.value]), false) == \(boolSettingLiteral)")) + ) GROUP BY \(interaction[.threadId]) ) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id]) """ diff --git a/SessionMessagingKit/Utilities/Preferences.swift b/SessionMessagingKit/Utilities/Preferences.swift index 539dafd56..4d39be274 100644 --- a/SessionMessagingKit/Utilities/Preferences.swift +++ b/SessionMessagingKit/Utilities/Preferences.swift @@ -66,6 +66,10 @@ public extension Setting.BoolKey { /// Controls whether the device should show screenshot notifications in one-to-one conversations (will always /// send screenshot notifications, this just controls whether they get filtered out or not) static let showScreenshotNotifications: Setting.BoolKey = "showScreenshotNotifications" + + /// Controls whether concurrent audio messages should automatically be played after the one the user starts + /// playing finishes + static let shouldAutoPlayConsecutiveAudioMessages: Setting.BoolKey = "shouldAutoPlayConsecutiveAudioMessages" } public extension Setting.StringKey { diff --git a/SessionUIKit/Components/OutlineButton.swift b/SessionUIKit/Components/OutlineButton.swift index e40a02f94..e2b088c1d 100644 --- a/SessionUIKit/Components/OutlineButton.swift +++ b/SessionUIKit/Components/OutlineButton.swift @@ -17,6 +17,14 @@ public final class OutlineButton: UIButton { case large } + public override var isEnabled: Bool { + didSet { + self.alpha = (isEnabled ? 1 : 0.5) + } + } + + // MARK: - Initialization + public init(style: Style, size: Size) { super.init(frame: .zero) diff --git a/SessionUIKit/Style Guide/Themes/UIKit+Theme.swift b/SessionUIKit/Style Guide/Themes/UIKit+Theme.swift index a80511fee..b9e4a9038 100644 --- a/SessionUIKit/Style Guide/Themes/UIKit+Theme.swift +++ b/SessionUIKit/Style Guide/Themes/UIKit+Theme.swift @@ -123,7 +123,7 @@ public extension UIProgressView { } } -public extension UITableViewRowAction { +public extension UIContextualAction { var themeBackgroundColor: ThemeValue? { set { ThemeManager.set(self, keyPath: \.backgroundColor, to: newValue) } get { return nil } diff --git a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift index 26214ffeb..ac96a8dc6 100644 --- a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift +++ b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift @@ -141,7 +141,6 @@ public class PagedDatabaseObserver: TransactionObserver where // The 'event' object only exists during this method so we need to copy the info // from it, otherwise it will cease to exist after this metod call finishes - changesInCommit.mutate { $0.insert(PagedData.TrackedChange(event: event)) } changesInCommit.mutate { $0.insert(trackedChange) } } @@ -288,9 +287,14 @@ public class PagedDatabaseObserver: TransactionObserver where } // Fetch the indexes of the rowIds so we can determine whether they should be added to the screen + let directRowIds: Set = changesToQuery.map { $0.rowId }.asSet() + let pagedRowIdsForRelatedDeletions: Set = relatedDeletions + .compactMap { $0.pagedRowIdsForRelatedDeletion } + .flatMap { $0 } + .asSet() let itemIndexes: [PagedData.RowIndexInfo] = PagedData.indexes( db, - rowIds: changesToQuery.map { $0.rowId }, + rowIds: Array(directRowIds), tableName: pagedTableName, requiredJoinSQL: joinSQL, orderSQL: orderSQL, @@ -306,9 +310,7 @@ public class PagedDatabaseObserver: TransactionObserver where ) let relatedDeletionIndexes: [PagedData.RowIndexInfo] = PagedData.indexes( db, - rowIds: relatedDeletions - .compactMap { $0.pagedRowIdsForRelatedDeletion } - .flatMap { $0 }, + rowIds: Array(pagedRowIdsForRelatedDeletions), tableName: pagedTableName, requiredJoinSQL: joinSQL, orderSQL: orderSQL, @@ -353,6 +355,37 @@ public class PagedDatabaseObserver: TransactionObserver where let validRelatedDeletionRowIds: [Int64] = determineValidChanges(for: relatedDeletionIndexes) let countBefore: Int = itemIndexes.filter { $0.rowIndex < updatedPageInfo.pageOffset }.count + // If the number of indexes doesn't match the number of rowIds then it means something changed + // resulting in an item being filtered out + func performRemovalsIfNeeded(for rowIds: Set, indexes: [PagedData.RowIndexInfo]) { + let uniqueIndexes: Set = indexes.map { $0.rowId }.asSet() + + // If they have the same count then nothin was filtered out so do nothing + guard rowIds.count != uniqueIndexes.count else { return } + + // Otherwise something was probably removed so try to remove it from the cache + let rowIdsRemoved: Set = rowIds.subtracting(uniqueIndexes) + let preDeletionCount: Int = updatedDataCache.count + updatedDataCache = updatedDataCache.deleting(rowIds: Array(rowIdsRemoved)) + + // Lastly make sure there were actually changes before updating the page info + guard updatedDataCache.count != preDeletionCount else { return } + + let dataSizeDiff: Int = (updatedDataCache.count - preDeletionCount) + + updatedPageInfo = PagedData.PageInfo( + pageSize: updatedPageInfo.pageSize, + pageOffset: updatedPageInfo.pageOffset, + currentCount: (updatedPageInfo.currentCount + dataSizeDiff), + totalCount: (updatedPageInfo.totalCount + dataSizeDiff) + ) + } + + // Actually perform any required removals + performRemovalsIfNeeded(for: directRowIds, indexes: itemIndexes) + performRemovalsIfNeeded(for: pagedRowIdsForRelatedChanges, indexes: relatedChangeIndexes) + performRemovalsIfNeeded(for: pagedRowIdsForRelatedDeletions, indexes: relatedDeletionIndexes) + // Update the offset and totalCount even if the rows are outside of the current page (need to // in order to ensure the 'load more' sections are accurate) updatedPageInfo = PagedData.PageInfo( @@ -374,7 +407,7 @@ public class PagedDatabaseObserver: TransactionObserver where updateDataAndCallbackIfNeeded(updatedDataCache, updatedPageInfo, true) return } - + // Fetch the inserted/updated rows let targetRowIds: [Int64] = Array((validChangeRowIds + validRelatedChangeRowIds + validRelatedDeletionRowIds).asSet()) let updatedItems: [T] = (try? dataQuery(targetRowIds)