From f07313c7acfc63386719d76a13c3a1f954e82246 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 19 Jun 2023 18:19:47 +1000 Subject: [PATCH] Fixed a number of bugs found during internal testing Updated to the latest libSession to increase the available size for config message content (size check now happens after compression rather than before) Added some additional logs for config size info Fixed a bug where the database could be accessed before the migrations ran which could result in unexpected behaviours Fixed a bug where you couldn't mark a non one-to-one thread as read/unread Fixed a bug where a database initialization failure wouldn't result in a migration failure (user would be stuck on the splash screen indefinitely) Fixed a bug where if a message was too large for the screen the conversation would open it centered on the screen (now it will be positioned to the top) Started looking at broken unit tests Increased the build number --- LibSession-Util | 2 +- Session.xcodeproj/project.pbxproj | 38 +-- .../ConversationVC+Interaction.swift | 19 +- Session/Conversations/ConversationVC.swift | 242 +++++++++++------- .../Conversations/ConversationViewModel.swift | 30 ++- .../Message Cells/MessageCell.swift | 1 + .../Message Cells/UnreadMarkerCell.swift | 73 ++++++ .../Message Cells/VisibleMessageCell.swift | 2 +- ...ttomButton.swift => RoundIconButton.swift} | 35 +-- Session/Home/HomeVC.swift | 7 +- Session/Meta/AppDelegate.swift | 15 +- .../Translations/de.lproj/Localizable.strings | 2 + .../Translations/en.lproj/Localizable.strings | 2 + .../Translations/es.lproj/Localizable.strings | 2 + .../Translations/fa.lproj/Localizable.strings | 2 + .../Translations/fi.lproj/Localizable.strings | 2 + .../Translations/fr.lproj/Localizable.strings | 2 + .../Translations/hi.lproj/Localizable.strings | 2 + .../Translations/hr.lproj/Localizable.strings | 2 + .../id-ID.lproj/Localizable.strings | 2 + .../Translations/it.lproj/Localizable.strings | 2 + .../Translations/ja.lproj/Localizable.strings | 2 + .../Translations/nl.lproj/Localizable.strings | 2 + .../Translations/pl.lproj/Localizable.strings | 2 + .../pt_BR.lproj/Localizable.strings | 2 + .../Translations/ru.lproj/Localizable.strings | 2 + .../Translations/si.lproj/Localizable.strings | 2 + .../Translations/sk.lproj/Localizable.strings | 2 + .../Translations/sv.lproj/Localizable.strings | 2 + .../Translations/th.lproj/Localizable.strings | 2 + .../vi-VN.lproj/Localizable.strings | 2 + .../zh-Hant.lproj/Localizable.strings | 2 + .../zh_CN.lproj/Localizable.strings | 2 + Session/Shared/ScreenLockUI.swift | 4 +- Session/Utilities/MockDataGenerator.swift | 69 +---- .../Config Handling/SessionUtil+Shared.swift | 2 +- .../SessionUtil/SessionUtil.swift | 36 ++- .../Shared Models/MessageViewModel.swift | 1 + .../Configs/ConfigContactsSpec.swift | 109 +++++--- SessionUIKit/Style Guide/ThemeManager.swift | 6 +- .../Themes/Theme+ClassicDark.swift | 5 +- .../Themes/Theme+ClassicLight.swift | 5 +- .../Style Guide/Themes/Theme+OceanDark.swift | 5 +- .../Style Guide/Themes/Theme+OceanLight.swift | 5 +- SessionUIKit/Style Guide/Themes/Theme.swift | 3 + SessionUtilitiesKit/Database/Storage.swift | 21 +- .../Database/StorageError.swift | 1 + .../Database/Types/Migration.swift | 15 +- .../DatabaseMigrator+Utilities.swift | 9 +- .../Utilities/ARC4RandomNumberGenerator.swift | 73 ++++++ .../PersistableRecordUtilitiesSpec.swift | 3 +- .../ScreenLockViewController.swift | 2 +- 52 files changed, 596 insertions(+), 286 deletions(-) create mode 100644 Session/Conversations/Message Cells/UnreadMarkerCell.swift rename Session/Conversations/Views & Modals/{ScrollToBottomButton.swift => RoundIconButton.swift} (67%) create mode 100644 SessionUtilitiesKit/Utilities/ARC4RandomNumberGenerator.swift diff --git a/LibSession-Util b/LibSession-Util index 9777b37e8..49c78682a 160000 --- a/LibSession-Util +++ b/LibSession-Util @@ -1 +1 @@ -Subproject commit 9777b37e8545febcc082578341352dba7433db21 +Subproject commit 49c78682a6f4546c8773113f3e201244f0b1e65a diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 44dae29d6..6ef13920c 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -229,7 +229,7 @@ B88FA7FB26114EA70049422F /* Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88FA7FA26114EA70049422F /* Hex.swift */; }; B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; }; B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; }; - B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */; }; + B897621C25D201F7004F83B2 /* RoundIconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* RoundIconButton.swift */; }; B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B320B6258C30D70020074B /* HTMLMetadata.swift */; }; B8B558F126C4BB0600693325 /* CameraManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B558F026C4BB0600693325 /* CameraManager.swift */; }; B8B558FF26C4E05E00693325 /* WebRTCSession+MessageHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B558FE26C4E05E00693325 /* WebRTCSession+MessageHandling.swift */; }; @@ -746,6 +746,8 @@ FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */; }; FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7432804EF1B004C14C5 /* JobRunner.swift */; }; FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F68280F9EDA002CFC5D /* JobRunnerError.swift */; }; + FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */; }; + FD97B2422A3FEBF30027DD57 /* UnreadMarkerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */; }; FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */; }; FDA1E83629A5748F00C5C3BD /* ConfigUserGroupsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1E83529A5748F00C5C3BD /* ConfigUserGroupsSpec.swift */; }; FDA1E83929A5771A00C5C3BD /* LibSessionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1E83829A5771A00C5C3BD /* LibSessionSpec.swift */; }; @@ -1361,7 +1363,7 @@ B88FA7FA26114EA70049422F /* Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hex.swift; sourceTree = ""; }; B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = ""; }; B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = ""; }; - B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = ""; }; + B897621B25D201F7004F83B2 /* RoundIconButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundIconButton.swift; sourceTree = ""; }; B8B320B6258C30D70020074B /* HTMLMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadata.swift; sourceTree = ""; }; B8B558F026C4BB0600693325 /* CameraManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraManager.swift; sourceTree = ""; }; B8B558FE26C4E05E00693325 /* WebRTCSession+MessageHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+MessageHandling.swift"; sourceTree = ""; }; @@ -1795,6 +1797,7 @@ FD5C7304284F0FF30029977D /* MessageReceiver+VisibleMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+VisibleMessages.swift"; sourceTree = ""; }; FD5C7306284F103B0029977D /* MessageReceiver+MessageRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+MessageRequests.swift"; sourceTree = ""; }; FD5C7308285007920029977D /* BlindedIdLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdLookup.swift; sourceTree = ""; }; + FD5CE3442A3C5D96001A6DE3 /* DecryptExportedKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecryptExportedKey.swift; sourceTree = ""; }; FD5D201D27B0D87C00FEA984 /* SessionId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionId.swift; sourceTree = ""; }; FD6A7A682818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetrieveDefaultOpenGroupRoomsJob.swift; sourceTree = ""; }; FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateProfilePictureJob.swift; sourceTree = ""; }; @@ -1881,6 +1884,8 @@ FD8ECF912938552800C0D1BB /* Threading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = ""; }; FD8ECF93293856AF00C0D1BB /* Randomness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Randomness.swift; sourceTree = ""; }; FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = ""; }; + FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARC4RandomNumberGenerator.swift; sourceTree = ""; }; + FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnreadMarkerCell.swift; sourceTree = ""; }; FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = ""; }; FDA1E83529A5748F00C5C3BD /* ConfigUserGroupsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigUserGroupsSpec.swift; sourceTree = ""; }; FDA1E83829A5771A00C5C3BD /* LibSessionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionSpec.swift; sourceTree = ""; }; @@ -2483,7 +2488,7 @@ B821493625D4D6A7009C0F2A /* Views & Modals */ = { isa = PBXGroup; children = ( - B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */, + B897621B25D201F7004F83B2 /* RoundIconButton.swift */, B82149C025D605C6009C0F2A /* InfoBanner.swift */, C374EEEA25DA3CA70073A857 /* ConversationTitleView.swift */, FD4B200D283492210034334B /* InsetLockableTableView.swift */, @@ -2519,6 +2524,7 @@ 7B0EFDEF275084AA00FFAAE7 /* CallMessageCell.swift */, B8041AA625C90927003C2166 /* TypingIndicatorCell.swift */, FDB7400C28EBEC240094D718 /* DateHeaderCell.swift */, + FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */, ); path = "Message Cells"; sourceTree = ""; @@ -2958,7 +2964,6 @@ children = ( C33FD9B7255A54A300E217F9 /* Meta */, C36096ED25AD20FD008B62B2 /* Media Viewing & Editing */, - FD16AB5D2A1DD8E70083D849 /* Profile Pictures */, C36096EE25AD21BC008B62B2 /* Screen Lock */, C3851CD225624B060061EEB0 /* Shared Views */, C360970125AD22D3008B62B2 /* Shared View Controllers */, @@ -3592,6 +3597,7 @@ FD09796527F6B0A800936362 /* Utilities */ = { isa = PBXGroup; children = ( + FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */, FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */, FD3003692A3ADD6000B5A5FB /* CExceptionHelper.h */, FD30036D2A3AE26000B5A5FB /* CExceptionHelper.mm */, @@ -3635,13 +3641,6 @@ path = Models; sourceTree = ""; }; - FD16AB5D2A1DD8E70083D849 /* Profile Pictures */ = { - isa = PBXGroup; - children = ( - ); - path = "Profile Pictures"; - sourceTree = ""; - }; FD17D79427F3E03300122BE0 /* Migrations */ = { isa = PBXGroup; children = ( @@ -4289,6 +4288,7 @@ children = ( FDE7214F287E50D50093DF33 /* ProtoWrappers.py */, FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */, + FD5CE3442A3C5D96001A6DE3 /* DecryptExportedKey.swift */, ); path = Scripts; sourceTree = ""; @@ -5645,6 +5645,7 @@ FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */, C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */, C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */, + FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */, FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */, FD30036E2A3AE26000B5A5FB /* CExceptionHelper.mm in Sources */, C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */, @@ -5920,6 +5921,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + FD97B2422A3FEBF30027DD57 /* UnreadMarkerCell.swift in Sources */, FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */, 7B2561C429874851005C086C /* SessionCarouselView+Info.swift in Sources */, FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */, @@ -6103,7 +6105,7 @@ 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */, FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */, FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */, - B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */, + B897621C25D201F7004F83B2 /* RoundIconButton.swift in Sources */, 346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */, FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */, C302093E25DCBF08001F572D /* MentionSelectionView.swift in Sources */, @@ -6393,7 +6395,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 407; + CURRENT_PROJECT_VERSION = 408; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6465,7 +6467,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 407; + CURRENT_PROJECT_VERSION = 408; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6530,7 +6532,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 407; + CURRENT_PROJECT_VERSION = 408; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6604,7 +6606,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 407; + CURRENT_PROJECT_VERSION = 408; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -7512,7 +7514,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 407; + CURRENT_PROJECT_VERSION = 408; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7583,7 +7585,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 407; + CURRENT_PROJECT_VERSION = 408; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 715a4ab34..315480a08 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -16,7 +16,6 @@ extension ConversationVC: InputViewDelegate, MessageCellDelegate, ContextMenuActionDelegate, - ScrollToBottomButtonDelegate, SendMediaNavDelegate, UIDocumentPickerDelegate, AttachmentApprovalViewControllerDelegate, @@ -51,15 +50,6 @@ extension ConversationVC: navigationController?.pushViewController(viewController, animated: true) } - // MARK: - ScrollToBottomButtonDelegate - - func handleScrollToBottomButtonTapped() { - // The table view's content size is calculated by the estimated height of cells, - // so the result may be inaccurate before all the cells are loaded. Use this - // to scroll to the last row instead. - scrollToBottom(isAnimated: true) - } - // MARK: - Call @objc func startCall(_ sender: Any?) { @@ -858,10 +848,7 @@ extension ConversationVC: UIView.animate( withDuration: 0.25, - animations: { - self?.scrollButton.alpha = (self?.getScrollButtonOpacity() ?? 0) - self?.unreadCountView.alpha = (self?.scrollButton.alpha ?? 0) - }, + animations: { self?.updateScrollToBottom() }, completion: { _ in guard let contentOffset: CGPoint = self?.tableView.contentOffset else { return } @@ -1052,7 +1039,7 @@ extension ConversationVC: return } - self.scrollToInteractionIfNeeded(with: interactionInfo, highlight: true) + self.scrollToInteractionIfNeeded(with: interactionInfo, focusBehaviour: .highlight) } else if let linkPreview: LinkPreview = cellViewModel.linkPreview { switch linkPreview.variant { @@ -1725,7 +1712,7 @@ extension ConversationVC: func copy(_ cellViewModel: MessageViewModel) { switch cellViewModel.cellType { - case .typingIndicator, .dateHeader: break + case .typingIndicator, .dateHeader, .unreadMarker: break case .textOnlyMessage: if cellViewModel.body == nil, let linkPreview: LinkPreview = cellViewModel.linkPreview { diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 37a218c4f..c52f8d1cb 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -26,6 +26,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers private var hasReloadedThreadDataAfterDisappearance: Bool = true var focusedInteractionInfo: Interaction.TimestampInfo? + var focusBehaviour: ConversationViewModel.FocusBehaviour = .none var shouldHighlightNextScrollToInteraction: Bool = false // Search @@ -157,6 +158,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers ) result.registerHeaderFooterView(view: UITableViewHeaderFooterView.self) result.register(view: DateHeaderCell.self) + result.register(view: UnreadMarkerCell.self) result.register(view: VisibleMessageCell.self) result.register(view: InfoMessageCell.self) result.register(view: TypingIndicatorCell.self) @@ -182,6 +184,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers result.set(.width, greaterThanOrEqualTo: ConversationVC.unreadCountViewSize) result.set(.height, to: ConversationVC.unreadCountViewSize) result.isHidden = true + result.alpha = 0 return result }() @@ -253,7 +256,20 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers return result }() - lazy var scrollButton: ScrollToBottomButton = ScrollToBottomButton(delegate: self) + lazy var scrollButton: RoundIconButton = { + let result: RoundIconButton = RoundIconButton( + image: UIImage(named: "ic_chevron_down")? + .withRenderingMode(.alwaysTemplate) + ) { [weak self] in + // The table view's content size is calculated by the estimated height of cells, + // so the result may be inaccurate before all the cells are loaded. Use this + // to scroll to the last row instead. + self?.scrollToBottom(isAnimated: true) + } + result.alpha = 0 + + return result + }() lazy var messageRequestBackgroundView: UIView = { let result: UIView = UIView() @@ -936,7 +952,6 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers .firstIndex(where: { item -> Bool in // Since the first item is probably a `DateHeaderCell` (which would likely // be removed when inserting items above it) we check if the id matches - // either the first or second item let messages: [MessageViewModel] = self.viewModel .interactionData[oldSectionIndex] .elements @@ -992,8 +1007,8 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers self?.searchController.resultsBar.stopLoading() self?.scrollToInteractionIfNeeded( with: focusedInteractionInfo, - isAnimated: true, - highlight: (self?.shouldHighlightNextScrollToInteraction == true) + focusBehaviour: (self?.shouldHighlightNextScrollToInteraction == true ? .highlight : .none), + isAnimated: true ) if wasLoadingMore { @@ -1020,8 +1035,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers } else { // Need to update the scroll button alpha in case new messages were added but we didn't scroll - self.scrollButton.alpha = self.getScrollButtonOpacity() - self.unreadCountView.alpha = self.scrollButton.alpha + self.updateScrollToBottom() } return } @@ -1070,8 +1084,8 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers self?.searchController.resultsBar.stopLoading() self?.scrollToInteractionIfNeeded( with: focusedInteractionInfo, - isAnimated: true, - highlight: (self?.shouldHighlightNextScrollToInteraction == true) + focusBehaviour: (self?.shouldHighlightNextScrollToInteraction == true ? .highlight : .none), + isAnimated: true ) } } @@ -1090,8 +1104,8 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers self?.searchController.resultsBar.stopLoading() self?.scrollToInteractionIfNeeded( with: focusedInteractionInfo, - isAnimated: true, - highlight: (self?.shouldHighlightNextScrollToInteraction == true) + focusBehaviour: (self?.shouldHighlightNextScrollToInteraction == true ? .highlight : .none), + isAnimated: true ) // Complete page loading @@ -1132,14 +1146,17 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers // When the unread message count is more than the number of view items of a page, // the screen will scroll to the bottom instead of the first unread message if let focusedInteractionInfo: Interaction.TimestampInfo = self.viewModel.focusedInteractionInfo { - self.scrollToInteractionIfNeeded(with: focusedInteractionInfo, isAnimated: false, highlight: true) + self.scrollToInteractionIfNeeded( + with: focusedInteractionInfo, + focusBehaviour: self.viewModel.focusBehaviour, + isAnimated: false + ) } else { self.scrollToBottom(isAnimated: false) } - self.scrollButton.alpha = self.getScrollButtonOpacity() - self.unreadCountView.alpha = self.scrollButton.alpha + self.updateScrollToBottom() self.hasPerformedInitialScroll = true // Now that the data has loaded we need to check if either of the "load more" sections are @@ -1327,10 +1344,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers self?.messageRequestsViewBotomConstraint?.constant = -(keyboardTop + 12) self?.tableView.contentInset = newContentInset self?.tableView.contentOffset.y = newContentOffsetY - - let scrollButtonOpacity: CGFloat = (self?.getScrollButtonOpacity() ?? 0) - self?.scrollButton.alpha = scrollButtonOpacity - self?.unreadCountView.alpha = scrollButtonOpacity + self?.updateScrollToBottom() self?.view.setNeedsLayout() self?.view.layoutIfNeeded() @@ -1372,10 +1386,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers animations: { [weak self] in self?.scrollButtonBottomConstraint?.constant = -(keyboardTop + 12) self?.messageRequestsViewBotomConstraint?.constant = -(keyboardTop + 12) - - let scrollButtonOpacity: CGFloat = (self?.getScrollButtonOpacity() ?? 0) - self?.scrollButton.alpha = scrollButtonOpacity - self?.unreadCountView.alpha = scrollButtonOpacity + self?.updateScrollToBottom() self?.view.setNeedsLayout() self?.view.layoutIfNeeded() @@ -1584,48 +1595,13 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers } func scrollViewDidScroll(_ scrollView: UIScrollView) { - self.scrollButton.alpha = self.getScrollButtonOpacity() - self.unreadCountView.alpha = self.scrollButton.alpha + self.updateScrollToBottom() // The initial scroll can trigger this logic but we already mark the initially focused message // as read so don't run the below until the user actually scrolls after the initial layout guard self.didFinishInitialLayout else { return } - // We want to mark messages as read while we scroll, so grab the newest message and mark - // everything older as read - // - // Note: For the 'tableVisualBottom' we remove the 'Values.mediumSpacing' as that is the distance - // the table content appears above the input view - let tableVisualBottom: CGFloat = (tableView.frame.maxY - (tableView.contentInset.bottom - Values.mediumSpacing)) - - if - let visibleIndexPaths: [IndexPath] = self.tableView.indexPathsForVisibleRows, - let messagesSection: Int = visibleIndexPaths - .first(where: { self.viewModel.interactionData[$0.section].model == .messages })? - .section, - let newestCellViewModel: MessageViewModel = visibleIndexPaths - .sorted() - .filter({ $0.section == messagesSection }) - .compactMap({ indexPath -> (frame: CGRect, cellViewModel: MessageViewModel)? in - guard let frame: CGRect = tableView.cellForRow(at: indexPath)?.frame else { - return nil - } - - return ( - view.convert(frame, from: tableView), - self.viewModel.interactionData[indexPath.section].elements[indexPath.row] - ) - }) - // Exclude messages that are partially off the bottom of the screen - .filter({ $0.frame.maxY <= tableVisualBottom }) - .last? - .cellViewModel - { - self.viewModel.markAsRead( - target: .threadAndInteractions(interactionsBeforeInclusive: newestCellViewModel.id), - timestampMs: newestCellViewModel.timestampMs - ) - } + self.markFullyVisibleAndOlderCellsAsRead(interactionInfo: nil) } func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { @@ -1634,12 +1610,16 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers self.shouldHighlightNextScrollToInteraction else { self.focusedInteractionInfo = nil + self.focusBehaviour = .none self.shouldHighlightNextScrollToInteraction = false return } + let behaviour: ConversationViewModel.FocusBehaviour = self.focusBehaviour + DispatchQueue.main.async { [weak self] in - self?.highlightCellIfNeeded(interactionId: focusedInteractionInfo.id) + self?.markFullyVisibleAndOlderCellsAsRead(interactionInfo: focusedInteractionInfo) + self?.highlightCellIfNeeded(interactionId: focusedInteractionInfo.id, behaviour: behaviour) } } @@ -1650,12 +1630,20 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers unreadCountLabel.font = .boldSystemFont(ofSize: fontSize) unreadCountView.isHidden = (unreadCount == 0) } - - func getScrollButtonOpacity() -> CGFloat { - let contentOffsetY = tableView.contentOffset.y + + public func updateScrollToBottom() { + // The initial scroll can trigger this logic but we already mark the initially focused message + // as read so don't run the below until the user actually scrolls after the initial layout + guard self.didFinishInitialLayout else { return } + + // Calculate the target opacity for the scroll button + let contentOffsetY: CGFloat = tableView.contentOffset.y let x = (lastPageTop - ConversationVC.bottomInset - contentOffsetY).clamp(0, .greatestFiniteMagnitude) let a = 1 / (ConversationVC.scrollButtonFullVisibilityThreshold - ConversationVC.scrollButtonNoVisibilityThreshold) - return max(0, min(1, a * x)) + let targetOpacity: CGFloat = max(0, min(1, a * x)) + + self.scrollButton.alpha = targetOpacity + self.unreadCountView.alpha = targetOpacity } // MARK: - Search @@ -1769,23 +1757,19 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers } func conversationSearchController(_ conversationSearchController: ConversationSearchController, didSelectInteractionInfo interactionInfo: Interaction.TimestampInfo) { - scrollToInteractionIfNeeded(with: interactionInfo, highlight: true) + scrollToInteractionIfNeeded(with: interactionInfo, focusBehaviour: .highlight) } func scrollToInteractionIfNeeded( with interactionInfo: Interaction.TimestampInfo, + focusBehaviour: ConversationViewModel.FocusBehaviour = .none, position: UITableView.ScrollPosition = .middle, isJumpingToLastInteraction: Bool = false, - isAnimated: Bool = true, - highlight: Bool = false + isAnimated: Bool = true ) { // Store the info incase we need to load more data (call will be re-triggered) self.focusedInteractionInfo = interactionInfo - self.shouldHighlightNextScrollToInteraction = highlight - self.viewModel.markAsRead( - target: .threadAndInteractions(interactionsBeforeInclusive: interactionInfo.id), - timestampMs: interactionInfo.timestampMs - ) + self.shouldHighlightNextScrollToInteraction = (focusBehaviour == .highlight) // Ensure the target interaction has been loaded guard @@ -1819,16 +1803,47 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers return } - let targetIndexPath: IndexPath = IndexPath( - row: targetMessageIndex, - section: messageSectionIndex - ) + // If it's before the initial layout and the index before the target is an 'UnreadMarker' then + // we should scroll to that instead (will be better UX) + let targetIndexPath: IndexPath = { + guard + !self.didFinishInitialLayout && + targetMessageIndex > 0 && + self.viewModel.interactionData[messageSectionIndex] + .elements[targetMessageIndex - 1] + .cellType == .unreadMarker + else { + return IndexPath( + row: targetMessageIndex, + section: messageSectionIndex + ) + } + + return IndexPath( + row: (targetMessageIndex - 1), + section: messageSectionIndex + ) + }() + let targetPosition: UITableView.ScrollPosition = { + guard position == .middle else { return position } + + // Make sure the target cell isn't too large for the screen (if it is then we want to scroll + // it to the top rather than the middle + let cellSize: CGSize = self.tableView( + tableView, + cellForRowAt: targetIndexPath + ).systemLayoutSizeFitting(view.bounds.size) + + guard cellSize.height > tableView.frame.size.height else { return position } + + return .top + }() // If we aren't animating or aren't highlighting then everything can be run immediately - guard isAnimated && highlight else { + guard isAnimated else { self.tableView.scrollToRow( at: targetIndexPath, - at: position, + at: targetPosition, animated: (self.didFinishInitialLayout && isAnimated) ) @@ -1837,16 +1852,17 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers // of messages) self.scrollViewDidScroll(self.tableView) - // If we haven't finished the initial layout then we want to delay the highlight slightly - // so it doesn't look buggy with the push transition - if highlight { - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(self.didFinishInitialLayout ? 0 : 150)) { [weak self] in - self?.highlightCellIfNeeded(interactionId: interactionInfo.id) - } + // If we haven't finished the initial layout then we want to delay the highlight/markRead slightly + // so it doesn't look buggy with the push transition and we know for sure the correct visible cells + // have been loaded + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(self.didFinishInitialLayout ? 0 : 150)) { [weak self] in + self?.markFullyVisibleAndOlderCellsAsRead(interactionInfo: interactionInfo) + self?.highlightCellIfNeeded(interactionId: interactionInfo.id, behaviour: focusBehaviour) } self.shouldHighlightNextScrollToInteraction = false self.focusedInteractionInfo = nil + self.focusBehaviour = .none return } @@ -1857,16 +1873,70 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers let targetRect: CGRect = self.tableView.rectForRow(at: targetIndexPath) guard !self.tableView.bounds.contains(targetRect) else { - self.highlightCellIfNeeded(interactionId: interactionInfo.id) + self.markFullyVisibleAndOlderCellsAsRead(interactionInfo: interactionInfo) + self.highlightCellIfNeeded(interactionId: interactionInfo.id, behaviour: focusBehaviour) return } - self.tableView.scrollToRow(at: targetIndexPath, at: position, animated: true) + self.tableView.scrollToRow(at: targetIndexPath, at: targetPosition, animated: true) } - func highlightCellIfNeeded(interactionId: Int64) { + func markFullyVisibleAndOlderCellsAsRead(interactionInfo: Interaction.TimestampInfo?) { + // We want to mark messages as read on load and while we scroll, so grab the newest message and mark + // everything older as read + // + // Note: For the 'tableVisualBottom' we remove the 'Values.mediumSpacing' as that is the distance + // the table content appears above the input view + let tableVisualBottom: CGFloat = (tableView.frame.maxY - (tableView.contentInset.bottom - Values.mediumSpacing)) + + guard + let visibleIndexPaths: [IndexPath] = self.tableView.indexPathsForVisibleRows, + let messagesSection: Int = visibleIndexPaths + .first(where: { self.viewModel.interactionData[$0.section].model == .messages })? + .section, + let newestCellViewModel: MessageViewModel = visibleIndexPaths + .sorted() + .filter({ $0.section == messagesSection }) + .compactMap({ indexPath -> (frame: CGRect, cellViewModel: MessageViewModel)? in + guard let cell: VisibleMessageCell = tableView.cellForRow(at: indexPath) as? VisibleMessageCell else { + return nil + } + + return ( + view.convert(cell.frame, from: tableView), + self.viewModel.interactionData[indexPath.section].elements[indexPath.row] + ) + }) + // Exclude messages that are partially off the bottom of the screen + .filter({ $0.frame.maxY <= tableVisualBottom }) + .last? + .cellViewModel + else { + // If we weren't able to get any visible cells for some reason then we should fall back to + // marking the provided interactionInfo as read just in case + if let interactionInfo: Interaction.TimestampInfo = interactionInfo { + self.viewModel.markAsRead( + target: .threadAndInteractions(interactionsBeforeInclusive: interactionInfo.id), + timestampMs: interactionInfo.timestampMs + ) + } + return + } + + // Mark all interactions before the newest entirely-visible one as read + self.viewModel.markAsRead( + target: .threadAndInteractions(interactionsBeforeInclusive: newestCellViewModel.id), + timestampMs: newestCellViewModel.timestampMs + ) + } + + func highlightCellIfNeeded(interactionId: Int64, behaviour: ConversationViewModel.FocusBehaviour) { self.shouldHighlightNextScrollToInteraction = false self.focusedInteractionInfo = nil + self.focusBehaviour = .none + + // Only trigger the highlight if that's the desired behaviour + guard behaviour == .highlight else { return } // Trigger on the next run loop incase we are still finishing some other animation DispatchQueue.main.async { diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index 9b891ce49..b0cbbdf34 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -9,6 +9,13 @@ import SessionUtilitiesKit public class ConversationViewModel: OWSAudioPlayerDelegate { public typealias SectionModel = ArraySection + // MARK: - FocusBehaviour + + public enum FocusBehaviour { + case none + case highlight + } + // MARK: - Action public enum Action { @@ -35,6 +42,8 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { public var sentMessageBeforeUpdate: Bool = false public var lastSearchedText: String? public let focusedInteractionInfo: Interaction.TimestampInfo? // Note: This is used for global search + public let focusBehaviour: FocusBehaviour + private let initialUnreadInteractionId: Int64? public lazy var blockedBannerMessage: String = { switch self.threadData.threadVariant { @@ -116,6 +125,13 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { self.threadId = threadId self.initialThreadVariant = threadVariant self.focusedInteractionInfo = initialData?.targetInteractionInfo + self.focusBehaviour = (focusedInteractionInfo == nil ? .none : .highlight) + self.initialUnreadInteractionId = (focusedInteractionInfo == nil ? + // If we didn't provide a 'focusedInteractionInfo' then 'initialData?.targetInteractionInfo?.id' will be + // the oldest unread interaction + initialData?.targetInteractionInfo?.id : + nil + ) self.threadData = SessionThreadViewModel( threadId: threadId, threadVariant: threadVariant, @@ -321,6 +337,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { } private func process(data: [MessageViewModel], for pageInfo: PagedData.PageInfo) -> [SectionModel] { + let initialUnreadInteractionId: Int64? = self.initialUnreadInteractionId let typingIndicator: MessageViewModel? = data.first(where: { $0.isTypingIndicator == true }) let sortedData: [MessageViewModel] = data .filter { $0.isTypingIndicator != true } @@ -362,11 +379,20 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { ) } .reduce([]) { result, message in + let updatedResult: [MessageViewModel] = result + .appending(initialUnreadInteractionId == nil || message.id != initialUnreadInteractionId ? + nil : + MessageViewModel( + timestampMs: message.timestampMs, + cellType: .unreadMarker + ) + ) + guard message.shouldShowDateHeader else { - return result.appending(message) + return updatedResult.appending(message) } - return result + return updatedResult .appending( MessageViewModel( timestampMs: message.timestampMs, diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 44886ba9c..dde344352 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -65,6 +65,7 @@ public class MessageCell: UITableViewCell { static func cellType(for viewModel: MessageViewModel) -> MessageCell.Type { guard viewModel.cellType != .typingIndicator else { return TypingIndicatorCell.self } guard viewModel.cellType != .dateHeader else { return DateHeaderCell.self } + guard viewModel.cellType != .unreadMarker else { return UnreadMarkerCell.self } switch viewModel.variant { case .standardOutgoing, .standardIncoming, .standardIncomingDeleted: diff --git a/Session/Conversations/Message Cells/UnreadMarkerCell.swift b/Session/Conversations/Message Cells/UnreadMarkerCell.swift new file mode 100644 index 000000000..76410c050 --- /dev/null +++ b/Session/Conversations/Message Cells/UnreadMarkerCell.swift @@ -0,0 +1,73 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SignalUtilitiesKit +import SessionUtilitiesKit +import SessionMessagingKit + +final class UnreadMarkerCell: MessageCell { + public static let height: CGFloat = 32 + + // MARK: - UI + + private let leftLine: UIView = { + let result: UIView = UIView() + result.themeBackgroundColor = .unreadMarker + result.set(.height, to: 1) // Intentionally 1 instead of 'separatorThickness' + + return result + }() + + private lazy var titleLabel: UILabel = { + let result = UILabel() + result.font = .boldSystemFont(ofSize: Values.smallFontSize) + result.text = "UNREAD_MESSAGES".localized() + result.themeTextColor = .unreadMarker + result.textAlignment = .center + + return result + }() + + private let rightLine: UIView = { + let result: UIView = UIView() + result.themeBackgroundColor = .unreadMarker + result.set(.height, to: 1) // Intentionally 1 instead of 'separatorThickness' + + return result + }() + + // MARK: - Initialization + + override func setUpViewHierarchy() { + super.setUpViewHierarchy() + + addSubview(leftLine) + addSubview(titleLabel) + addSubview(rightLine) + + leftLine.pin(.leading, to: .leading, of: self, withInset: Values.mediumSpacing) + leftLine.pin(.trailing, to: .leading, of: titleLabel, withInset: -Values.smallSpacing) + leftLine.center(.vertical, in: self) + titleLabel.center(.horizontal, in: self) + titleLabel.center(.vertical, in: self) + titleLabel.pin(.top, to: .top, of: self, withInset: Values.smallSpacing) + titleLabel.pin(.bottom, to: .bottom, of: self, withInset: -Values.smallSpacing) + rightLine.pin(.leading, to: .trailing, of: titleLabel, withInset: Values.smallSpacing) + rightLine.pin(.trailing, to: .trailing, of: self, withInset: -Values.mediumSpacing) + rightLine.center(.vertical, in: self) + } + + // MARK: - Updating + + override func update( + with cellViewModel: MessageViewModel, + mediaCache: NSCache, + playbackInfo: ConversationViewModel.PlaybackInfo?, + showExpandedReactions: Bool, + lastSearchText: String? + ) { + guard cellViewModel.cellType == .unreadMarker else { return } + } + + override func dynamicUpdate(with cellViewModel: MessageViewModel, playbackInfo: ConversationViewModel.PlaybackInfo?) {} +} diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 79b137e5d..509e7592c 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -489,7 +489,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } switch cellViewModel.cellType { - case .typingIndicator, .dateHeader: break + case .typingIndicator, .dateHeader, .unreadMarker: break case .textOnlyMessage: let inset: CGFloat = 12 diff --git a/Session/Conversations/Views & Modals/ScrollToBottomButton.swift b/Session/Conversations/Views & Modals/RoundIconButton.swift similarity index 67% rename from Session/Conversations/Views & Modals/ScrollToBottomButton.swift rename to Session/Conversations/Views & Modals/RoundIconButton.swift index 413dbdbb2..74e6a7978 100644 --- a/Session/Conversations/Views & Modals/ScrollToBottomButton.swift +++ b/Session/Conversations/Views & Modals/RoundIconButton.swift @@ -3,8 +3,8 @@ import UIKit import SessionUIKit -final class ScrollToBottomButton: UIView { - private weak var delegate: ScrollToBottomButtonDelegate? +final class RoundIconButton: UIView { + private let onTap: () -> () // MARK: - Settings @@ -13,12 +13,12 @@ final class ScrollToBottomButton: UIView { // MARK: - Lifecycle - init(delegate: ScrollToBottomButtonDelegate) { - self.delegate = delegate + init(image: UIImage?, onTap: @escaping () -> ()) { + self.onTap = onTap super.init(frame: CGRect.zero) - setUpViewHierarchy() + setUpViewHierarchy(image: image) } override init(frame: CGRect) { @@ -29,7 +29,7 @@ final class ScrollToBottomButton: UIView { preconditionFailure("Use init(delegate:) instead.") } - private func setUpViewHierarchy() { + private func setUpViewHierarchy(image: UIImage?) { // Background & blur let backgroundView = UIView() backgroundView.themeBackgroundColor = .backgroundSecondary @@ -49,9 +49,9 @@ final class ScrollToBottomButton: UIView { } // Size & shape - set(.width, to: ScrollToBottomButton.size) - set(.height, to: ScrollToBottomButton.size) - layer.cornerRadius = (ScrollToBottomButton.size / 2) + set(.width, to: RoundIconButton.size) + set(.height, to: RoundIconButton.size) + layer.cornerRadius = (RoundIconButton.size / 2) layer.masksToBounds = true // Border @@ -59,16 +59,13 @@ final class ScrollToBottomButton: UIView { layer.borderWidth = Values.separatorThickness // Icon - let iconImageView = UIImageView( - image: UIImage(named: "ic_chevron_down")? - .withRenderingMode(.alwaysTemplate) - ) + let iconImageView = UIImageView(image: image) iconImageView.themeTintColor = .textPrimary iconImageView.contentMode = .scaleAspectFit addSubview(iconImageView) iconImageView.center(in: self) - iconImageView.set(.width, to: ScrollToBottomButton.iconSize) - iconImageView.set(.height, to: ScrollToBottomButton.iconSize) + iconImageView.set(.width, to: RoundIconButton.iconSize) + iconImageView.set(.height, to: RoundIconButton.iconSize) // Gesture recognizer let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) @@ -78,12 +75,6 @@ final class ScrollToBottomButton: UIView { // MARK: - Interaction @objc private func handleTap() { - delegate?.handleScrollToBottomButtonTapped() + onTap() } } - -// MARK: - ScrollToBottomButtonDelegate - -protocol ScrollToBottomButtonDelegate: AnyObject { - func handleScrollToBottomButtonTapped() -} diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index cca5b4dbb..eb7d34ec5 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -651,9 +651,10 @@ final class HomeVC: BaseVC, SessionUtilRespondingViewController, UITableViewData switch section.model { case .threads: // Cannot properly sync outgoing blinded message requests so don't provide the option - guard SessionId(from: section.elements[indexPath.row].threadId)?.prefix == .standard else { - return nil - } + guard + threadViewModel.threadVariant != .contact || + SessionId(from: section.elements[indexPath.row].threadId)?.prefix == .standard + else { return nil } return UIContextualAction.configuration( for: UIContextualAction.generateSwipeActions( diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 3de4c535c..dcab9b1d8 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -47,9 +47,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let mainWindow: UIWindow = TraitObservingWindow(frame: UIScreen.main.bounds) self.loadingViewController = LoadingViewController() - // Store a weak reference in the ThemeManager so it can properly apply themes as needed - ThemeManager.mainWindow = mainWindow - AppSetup.setupEnvironment( appSpecificBlock: { // Create AppEnvironment @@ -78,6 +75,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return } + /// Store a weak reference in the ThemeManager so it can properly apply themes as needed + /// + /// **Note:** Need to do this after the db migrations because theme preferences are stored in the database and + /// we don't want to access it until after the migrations run + ThemeManager.mainWindow = mainWindow self?.completePostMigrationSetup(calledFrom: .finishLaunching, needsConfigSync: needsConfigSync) } ) @@ -333,7 +335,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD private func showFailedMigrationAlert(calledFrom lifecycleMethod: LifecycleMethod, error: Error?) { let alert = UIAlertController( title: "Session", - message: "DATABASE_MIGRATION_FAILED".localized(), + message: { + switch (error ?? StorageError.generic) { + case StorageError.startupFailed: return "DATABASE_STARTUP_FAILED".localized() + default: return "DATABASE_MIGRATION_FAILED".localized() + } + }(), preferredStyle: .alert ) alert.addAction(UIAlertAction(title: "HELP_REPORT_BUG_ACTION_TITLE".localized(), style: .default) { _ in diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 692363dec..cf306a367 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index a17e9d9fa..0853ba3fb 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index 7f99b5324..2cd107fb4 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index d264984ad..721d742fe 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "متاسفانه خطایی رخ داده است"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "لطفا بعدا دوباره تلاش کنید"; "LOADING_CONVERSATIONS" = "درحال بارگزاری پیام ها..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "هنگام بهینه‌سازی پایگاه داده خطایی روی داد\n\nشما می‌توانید گزارش‌های برنامه خود را صادر کنید تا بتوانید برای عیب‌یابی به اشتراک بگذارید یا می‌توانید دستگاه خود را بازیابی کنید\n\nهشدار: بازیابی دستگاه شما منجر به از دست رفتن داده‌های قدیمی‌تر از دو هفته می‌شود."; "RECOVERY_PHASE_ERROR_GENERIC" = "مشکلی پیش آمد. لطفاً عبارت بازیابی خود را بررسی کنید و دوباره امتحان کنید."; "RECOVERY_PHASE_ERROR_LENGTH" = "به نظر می رسد کلمات کافی وارد نکرده اید. لطفاً عبارت بازیابی خود را بررسی کنید و دوباره امتحان کنید."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index b7dd60b4c..e06981e6c 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 3cbd8abc8..0f42d5c3b 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oups, une erreur est survenue"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Veuillez réessayer plus tard"; "LOADING_CONVERSATIONS" = "Chargement des conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "Une erreur est survenue pendant l'optimisation de la base de données\n\nVous pouvez exporter votre journal d'application pour le partager et aider à régler le problème ou vous pouvez restaurer votre appareil\n\nAttention : restaurer votre appareil résultera en une perte des données des deux dernières semaines"; "RECOVERY_PHASE_ERROR_GENERIC" = "Quelque chose s'est mal passé. Vérifiez votre phrase de récupération et réessayez s'il vous plaît."; "RECOVERY_PHASE_ERROR_LENGTH" = "Il semble que vous n'avez pas saisi tous les mots. Vérifiez votre phrase de récupération et réessayez s'il vous plaît."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index 3f8db15d5..bf51ebd6c 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index 78b23b6c5..5bac5abfb 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index ecb22004e..4e54eb039 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index b1648c3ec..b41f34667 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index 5048ae1a8..f2b83382f 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index ec4311bd3..0aaa027f7 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index 0284e903d..dbefe0039 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index 00fcb652b..888d81746 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index a6773a96d..77850b124 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index e530f410a..f28501061 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index 53bd20179..5a995249d 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index bca898792..688680b2a 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index 126504931..98ca3e5bb 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index ce9bd9a6a..aa5527991 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index 5e28a0543..8296fa917 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 4f5422166..68076c678 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -414,6 +414,7 @@ "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; "LOADING_CONVERSATIONS" = "Loading Conversations..."; +"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks"; "RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again."; "RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again."; @@ -637,6 +638,7 @@ "CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name"; "MARK_AS_READ" = "Mark Read"; "MARK_AS_UNREAD" = "Mark Unread"; +"UNREAD_MESSAGES" = "Unread Messages"; "CONVERSATION_EMPTY_STATE" = "You have no messages from %@. Send a message to start the conversation!"; "CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@."; "CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@."; diff --git a/Session/Shared/ScreenLockUI.swift b/Session/Shared/ScreenLockUI.swift index d61f842b0..dcf7d55c6 100644 --- a/Session/Shared/ScreenLockUI.swift +++ b/Session/Shared/ScreenLockUI.swift @@ -15,7 +15,7 @@ class ScreenLockUI { result.isHidden = false result.windowLevel = ._Background result.isOpaque = true - result.themeBackgroundColor = .backgroundPrimary + result.themeBackgroundColorForced = .theme(.classicDark, color: .backgroundPrimary) result.rootViewController = self.screenBlockingViewController return result @@ -291,7 +291,7 @@ class ScreenLockUI { window.isHidden = false window.windowLevel = ._Background window.isOpaque = true - window.themeBackgroundColor = .backgroundPrimary + window.themeBackgroundColorForced = .theme(.classicDark, color: .backgroundPrimary) let viewController: ScreenLockViewController = ScreenLockViewController { [weak self] in guard self?.appIsInactiveOrBackground == false else { diff --git a/Session/Utilities/MockDataGenerator.swift b/Session/Utilities/MockDataGenerator.swift index d4ab94abc..1cbf94fbe 100644 --- a/Session/Utilities/MockDataGenerator.swift +++ b/Session/Utilities/MockDataGenerator.swift @@ -6,67 +6,6 @@ import Curve25519Kit import SessionMessagingKit enum MockDataGenerator { - // Note: This was taken from TensorFlow's Random (https://github.com/apple/swift/blob/bc8f9e61d333b8f7a625f74d48ef0b554726e349/stdlib/public/TensorFlow/Random.swift) - // the complex approach is needed due to an issue with Swift's randomElement(using:) - // generation (see https://stackoverflow.com/a/64897775 for more info) - struct ARC4RandomNumberGenerator: RandomNumberGenerator { - var state: [UInt8] = Array(0...255) - var iPos: UInt8 = 0 - var jPos: UInt8 = 0 - - init(seed: T) { - self.init( - seed: (0..<(UInt64.bitWidth / UInt64.bitWidth)).map { index in - UInt8(truncatingIfNeeded: seed >> (UInt8.bitWidth * index)) - } - ) - } - - init(seed: [UInt8]) { - precondition(seed.count > 0, "Length of seed must be positive") - precondition(seed.count <= 256, "Length of seed must be at most 256") - - // Note: Have to use a for loop instead of a 'forEach' otherwise - // it doesn't work properly (not sure why...) - var j: UInt8 = 0 - for i: UInt8 in 0...255 { - j &+= S(i) &+ seed[Int(i) % seed.count] - swapAt(i, j) - } - } - - /// Produce the next random UInt64 from the stream, and advance the internal state - mutating func next() -> UInt64 { - // Note: Have to use a for loop instead of a 'forEach' otherwise - // it doesn't work properly (not sure why...) - var result: UInt64 = 0 - for _ in 0.. UInt8 { - return state[Int(index)] - } - - /// Helper to swap elements of the state - private mutating func swapAt(_ i: UInt8, _ j: UInt8) { - state.swapAt(Int(i), Int(j)) - } - - /// Generates the next byte in the keystream. - private mutating func nextByte() -> UInt8 { - iPos &+= 1 - jPos &+= S(iPos) - swapAt(iPos, jPos) - return S(S(iPos) &+ S(jPos)) - } - } - // MARK: - Generation static var printProgress: Bool = true @@ -125,7 +64,7 @@ enum MockDataGenerator { logProgress("DM Thread \(threadIndex)", "Start") - let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &dmThreadRandomGenerator) }) + let data: Data = Data(dmThreadRandomGenerator.nextBytes(count: 16)) let randomSessionId: String = try! Identity.generate(from: data).x25519KeyPair.hexEncodedPublicKey let isMessageRequest: Bool = Bool.random(using: &dmThreadRandomGenerator) let contactNameLength: Int = ((5..<20).randomElement(using: &dmThreadRandomGenerator) ?? 0) @@ -207,7 +146,7 @@ enum MockDataGenerator { logProgress("Closed Group Thread \(threadIndex)", "Start") - let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &cgThreadRandomGenerator) }) + let data: Data = Data(cgThreadRandomGenerator.nextBytes(count: 16)) let randomGroupPublicKey: String = try! Identity.generate(from: data).x25519KeyPair.hexEncodedPublicKey let groupNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0) let groupName: String = (0..? = nil var dumpResultLen: Int = 0 - config_dump(conf, &dumpResult, &dumpResultLen) + try CExceptionHelper.performSafely { + config_dump(conf, &dumpResult, &dumpResultLen) + } guard let dumpResult: UnsafeMutablePointer = dumpResult else { return nil } @@ -308,15 +310,40 @@ public enum SessionUtil { // Ensure we always check the required user config types for changes even if there is no dump // data yet (to deal with first launch cases) - return existingDumpVariants + return try existingDumpVariants .compactMap { variant -> OutgoingConfResult? in - SessionUtil + try SessionUtil .config(for: variant, publicKey: publicKey) .mutate { conf in // Check if the config needs to be pushed guard conf != nil && config_needs_push(conf) else { return nil } - let cPushData: UnsafeMutablePointer = config_push(conf) + var cPushData: UnsafeMutablePointer! + let configCountInfo: String = { + var result: String = "Invalid" + + try? CExceptionHelper.performSafely { + switch variant { + case .userProfile: result = "1 profile" + case .contacts: result = "\(contacts_size(conf)) contacts" + case .userGroups: result = "\(user_groups_size(conf)) group conversations" + case .convoInfoVolatile: result = "\(convo_info_volatile_size(conf)) volatile conversations" + } + } + + return result + }() + + do { + try CExceptionHelper.performSafely { + cPushData = config_push(conf) + } + } + catch { + SNLog("[libSession] Failed to generate push data for \(variant) config data, size: \(configCountInfo), error: \(error)") + throw error + } + let pushData: Data = Data( bytes: cPushData.pointee.config, count: cPushData.pointee.config_len @@ -328,6 +355,7 @@ public enum SessionUtil { ) let seqNo: Int64 = cPushData.pointee.seqno cPushData.deallocate() + SNLog("[libSession - DEBUG] Push data for \(variant) config data, size: \(configCountInfo), bytes: \(pushData.count)") return OutgoingConfResult( message: SharedConfigMessage( diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index 177dcbbbb..bc143b0e5 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -55,6 +55,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, case genericAttachment case typingIndicator case dateHeader + case unreadMarker } public var differenceIdentifier: Int64 { id } diff --git a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigContactsSpec.swift b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigContactsSpec.swift index a363422b1..666ca512d 100644 --- a/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigContactsSpec.swift +++ b/SessionMessagingKitTests/LibSessionUtil/Configs/ConfigContactsSpec.swift @@ -53,8 +53,15 @@ class ConfigContactsSpec { // MARK: -- it can catch size limit errors thrown when pushing it("can catch size limit errors thrown when pushing") { + var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000) + try (0..<10000).forEach { index in - var contact: contacts_contact = try createContact(for: index, in: conf, maxing: .allProperties) + var contact: contacts_contact = try createContact( + for: index, + in: conf, + rand: &randomGenerator, + maxing: .allProperties + ) contacts_set(conf, &contact) } @@ -70,12 +77,19 @@ class ConfigContactsSpec { // MARK: -- can catch size limit errors thrown when dumping it("can catch size limit errors thrown when dumping") { - try (0..<10000).forEach { index in - var contact: contacts_contact = try createContact(for: index, in: conf, maxing: .allProperties) + var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000) + + try (0..<100000).forEach { index in + var contact: contacts_contact = try createContact( + for: index, + in: conf, + rand: &randomGenerator, + maxing: .allProperties + ) contacts_set(conf, &contact) } - expect(contacts_size(conf)).to(equal(10000)) + expect(contacts_size(conf)).to(equal(100000)) expect(config_needs_push(conf)).to(beTrue()) expect(config_needs_dump(conf)).to(beTrue()) @@ -117,8 +131,14 @@ class ConfigContactsSpec { // MARK: -- has not changed the max empty records it("has not changed the max empty records") { - for index in (0..<10000) { - var contact: contacts_contact = try createContact(for: index, in: conf) + var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000) + + for index in (0..<100000) { + var contact: contacts_contact = try createContact( + for: index, + in: conf, + rand: &randomGenerator + ) contacts_set(conf, &contact) do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } } @@ -129,13 +149,20 @@ class ConfigContactsSpec { } // Check that the record count matches the maximum when we last checked - expect(numRecords).to(equal(1775)) + expect(numRecords).to(equal(2370)) } // MARK: -- has not changed the max name only records it("has not changed the max name only records") { - for index in (0..<10000) { - var contact: contacts_contact = try createContact(for: index, in: conf, maxing: [.name]) + var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000) + + for index in (0..<100000) { + var contact: contacts_contact = try createContact( + for: index, + in: conf, + rand: &randomGenerator, + maxing: [.name] + ) contacts_set(conf, &contact) do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } } @@ -146,13 +173,20 @@ class ConfigContactsSpec { } // Check that the record count matches the maximum when we last checked - expect(numRecords).to(equal(526)) + expect(numRecords).to(equal(796)) } // MARK: -- has not changed the max name and profile pic only records it("has not changed the max name and profile pic only records") { - for index in (0..<10000) { - var contact: contacts_contact = try createContact(for: index, in: conf, maxing: [.name, .profile_pic]) + var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000) + + for index in (0..<100000) { + var contact: contacts_contact = try createContact( + for: index, + in: conf, + rand: &randomGenerator, + maxing: [.name, .profile_pic] + ) contacts_set(conf, &contact) do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } } @@ -163,13 +197,20 @@ class ConfigContactsSpec { } // Check that the record count matches the maximum when we last checked - expect(numRecords).to(equal(184)) + expect(numRecords).to(equal(290)) } // MARK: -- has not changed the max filled records it("has not changed the max filled records") { - for index in (0..<10000) { - var contact: contacts_contact = try createContact(for: index, in: conf, maxing: .allProperties) + var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000) + + for index in (0..<100000) { + var contact: contacts_contact = try createContact( + for: index, + in: conf, + rand: &randomGenerator, + maxing: .allProperties + ) contacts_set(conf, &contact) do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } } @@ -180,7 +221,7 @@ class ConfigContactsSpec { } // Check that the record count matches the maximum when we last checked - expect(numRecords).to(equal(134)) + expect(numRecords).to(equal(236)) } } @@ -547,9 +588,10 @@ class ConfigContactsSpec { private static func createContact( for index: Int, in conf: UnsafeMutablePointer?, + rand: inout ARC4RandomNumberGenerator, maxing properties: [ContactProperty] = [] ) throws -> contacts_contact { - let postPrefixId: String = "050000000000000000000000000000000000000000000000000000000000000000" + let postPrefixId: String = "05\(rand.nextBytes(count: 32).toHexString())" let sessionId: String = ("05\(index)a" + postPrefixId.suffix(postPrefixId.count - "05\(index)a".count)) var cSessionId: [CChar] = sessionId.cArray.nullTerminated() var contact: contacts_contact = contacts_contact() @@ -569,33 +611,22 @@ class ConfigContactsSpec { case .mute_until: contact.mute_until = Int64.max case .name: - contact.name = String( - data: Data( - repeating: "A".data(using: .utf8)![0], - count: SessionUtil.libSessionMaxNameByteLength - ), - encoding: .utf8 - ).toLibSession() + contact.name = rand.nextBytes(count: SessionUtil.libSessionMaxNameByteLength) + .toHexString() + .toLibSession() case .nickname: - contact.nickname = String( - data: Data( - repeating: "A".data(using: .utf8)![0], - count: SessionUtil.libSessionMaxNameByteLength - ), - encoding: .utf8 - ).toLibSession() + contact.nickname = rand.nextBytes(count: SessionUtil.libSessionMaxNameByteLength) + .toHexString() + .toLibSession() case .profile_pic: contact.profile_pic = user_profile_pic( - url: String( - data: Data( - repeating: "A".data(using: .utf8)![0], - count: SessionUtil.libSessionMaxProfileUrlByteLength - ), - encoding: .utf8 - ).toLibSession(), - key: "qwerty78901234567890123456789012".data(using: .utf8)!.toLibSession() + url: rand.nextBytes(count: SessionUtil.libSessionMaxProfileUrlByteLength) + .toHexString() + .toLibSession(), + key: Data(rand.nextBytes(count: 32)) + .toLibSession() ) } } diff --git a/SessionUIKit/Style Guide/ThemeManager.swift b/SessionUIKit/Style Guide/ThemeManager.swift index fa165e996..5926a82d1 100644 --- a/SessionUIKit/Style Guide/ThemeManager.swift +++ b/SessionUIKit/Style Guide/ThemeManager.swift @@ -407,8 +407,10 @@ internal class ThemeApplier { .compactMap { $0?.clearingOtherAppliers() } .filter { $0.info != info } - // Automatically apply the theme immediately - self.apply(theme: ThemeManager.currentTheme, isInitialApplication: true) + // Automatically apply the theme immediately (if the database has been setup) + if Storage.hasCreatedValidInstance { + self.apply(theme: ThemeManager.currentTheme, isInitialApplication: true) + } } // MARK: - Functions diff --git a/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift b/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift index 28f8bec4d..1bd0e039a 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift @@ -115,6 +115,9 @@ internal enum Theme_ClassicDark: ThemeColors { // Profile .profileIcon: .primary, .profileIcon_greenPrimaryColor: .black, - .profileIcon_background: .white + .profileIcon_background: .white, + + // Unread Marker + .unreadMarker: .primary ] } diff --git a/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift b/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift index a04dda681..b659a95e0 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift @@ -115,6 +115,9 @@ internal enum Theme_ClassicLight: ThemeColors { // Profile .profileIcon: .primary, .profileIcon_greenPrimaryColor: .primary, - .profileIcon_background: .black + .profileIcon_background: .black, + + // Unread Marker + .unreadMarker: .black ] } diff --git a/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift b/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift index 4f6e1bf3f..a87cf4d4d 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift @@ -115,6 +115,9 @@ internal enum Theme_OceanDark: ThemeColors { // Profile .profileIcon: .primary, .profileIcon_greenPrimaryColor: .black, - .profileIcon_background: .white + .profileIcon_background: .white, + + // Unread Marker + .unreadMarker: .primary ] } diff --git a/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift b/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift index 97a3ed812..ec4df6764 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift @@ -115,6 +115,9 @@ internal enum Theme_OceanLight: ThemeColors { // Profile .profileIcon: .primary, .profileIcon_greenPrimaryColor: .primary, - .profileIcon_background: .oceanLight1 + .profileIcon_background: .oceanLight1, + + // Unread Marker + .unreadMarker: .black ] } diff --git a/SessionUIKit/Style Guide/Themes/Theme.swift b/SessionUIKit/Style Guide/Themes/Theme.swift index f846304ea..d34d1a702 100644 --- a/SessionUIKit/Style Guide/Themes/Theme.swift +++ b/SessionUIKit/Style Guide/Themes/Theme.swift @@ -204,6 +204,9 @@ public indirect enum ThemeValue: Hashable { case profileIcon case profileIcon_greenPrimaryColor case profileIcon_background + + // Unread Marker + case unreadMarker } // MARK: - ForcedThemeValue diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index 1a28da053..1ea913cb3 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -17,13 +17,16 @@ open class Storage { private static var databasePathShm: String { "\(Storage.sharedDatabaseDirectoryPath)/\(Storage.dbFileName)-shm" } private static var databasePathWal: String { "\(Storage.sharedDatabaseDirectoryPath)/\(Storage.dbFileName)-wal" } + public static var hasCreatedValidInstance: Bool { internalHasCreatedValidInstance.wrappedValue } public static var isDatabasePasswordAccessible: Bool { guard (try? getDatabaseCipherKeySpec()) != nil else { return false } return true } + private var startupError: Error? private let migrationsCompleted: Atomic = Atomic(false) + private static let internalHasCreatedValidInstance: Atomic = Atomic(false) internal let internalCurrentlyRunningMigration: Atomic<(identifier: TargetMigrations.Identifier, migration: Migration.Type)?> = Atomic(nil) public static let shared: Storage = Storage() @@ -52,8 +55,9 @@ open class Storage { // If a custom writer was provided then use that (for unit testing) guard customWriter == nil else { dbWriter = customWriter - isValid = true perform(migrations: (customMigrations ?? []), async: false, onProgressUpdate: nil, onComplete: { _, _ in }) + isValid = true + Storage.internalHasCreatedValidInstance.mutate { $0 = true } return } @@ -99,8 +103,9 @@ open class Storage { configuration: config ) isValid = true + Storage.internalHasCreatedValidInstance.mutate { $0 = true } } - catch {} + catch { startupError = error } } // MARK: - Migrations @@ -118,7 +123,12 @@ open class Storage { onProgressUpdate: ((CGFloat, TimeInterval) -> ())?, onComplete: @escaping (Swift.Result, Bool) -> () ) { - guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return } + guard isValid, let dbWriter: DatabaseWriter = dbWriter else { + let error: Error = (startupError ?? StorageError.startupFailed) + SNLog("[Database Error] Statup failed with error: \(error)") + onComplete(.failure(StorageError.startupFailed), false) + return + } typealias MigrationInfo = (identifier: TargetMigrations.Identifier, migrations: TargetMigrations.MigrationSet) let sortedMigrationInfo: [MigrationInfo] = migrations @@ -135,11 +145,11 @@ open class Storage { .reduce(into: []) { result, next in result.append(contentsOf: next) } // Setup and run any required migrations - migrator = { + migrator = { [weak self] in var migrator: DatabaseMigrator = DatabaseMigrator() sortedMigrationInfo.forEach { migrationInfo in migrationInfo.migrations.forEach { migration in - migrator.registerMigration(migrationInfo.identifier, migration: migration) + migrator.registerMigration(self, targetIdentifier: migrationInfo.identifier, migration: migration) } } @@ -316,6 +326,7 @@ open class Storage { try? SUKLegacy.deleteLegacyDatabaseFilesAndKey() Storage.shared.isValid = false + Storage.internalHasCreatedValidInstance.mutate { $0 = false } Storage.shared.migrationsCompleted.mutate { $0 = false } Storage.shared.dbWriter = nil diff --git a/SessionUtilitiesKit/Database/StorageError.swift b/SessionUtilitiesKit/Database/StorageError.swift index 7e48cabc5..b8fbfdf73 100644 --- a/SessionUtilitiesKit/Database/StorageError.swift +++ b/SessionUtilitiesKit/Database/StorageError.swift @@ -5,6 +5,7 @@ import Foundation public enum StorageError: Error { case generic case databaseInvalid + case startupFailed case migrationFailed case invalidKeySpec case decodingFailed diff --git a/SessionUtilitiesKit/Database/Types/Migration.swift b/SessionUtilitiesKit/Database/Types/Migration.swift index b0d87d187..6e4c909e5 100644 --- a/SessionUtilitiesKit/Database/Types/Migration.swift +++ b/SessionUtilitiesKit/Database/Types/Migration.swift @@ -13,15 +13,16 @@ public protocol Migration { } public extension Migration { - static func loggedMigrate(_ targetIdentifier: TargetMigrations.Identifier) -> ((_ db: Database) throws -> ()) { + static func loggedMigrate( + _ storage: Storage?, + targetIdentifier: TargetMigrations.Identifier + ) -> ((_ db: Database) throws -> ()) { return { (db: Database) in SNLogNotTests("[Migration Info] Starting \(targetIdentifier.key(with: self))") - Storage.shared.internalCurrentlyRunningMigration.mutate { $0 = (targetIdentifier, self) } - do { try migrate(db) } - catch { - Storage.shared.internalCurrentlyRunningMigration.mutate { $0 = nil } - throw error - } + storage?.internalCurrentlyRunningMigration.mutate { $0 = (targetIdentifier, self) } + defer { storage?.internalCurrentlyRunningMigration.mutate { $0 = nil } } + + try migrate(db) SNLogNotTests("[Migration Info] Completed \(targetIdentifier.key(with: self))") } } diff --git a/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift index 337dd805f..179a3edd3 100644 --- a/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift +++ b/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift @@ -4,10 +4,15 @@ import Foundation import GRDB public extension DatabaseMigrator { - mutating func registerMigration(_ targetIdentifier: TargetMigrations.Identifier, migration: Migration.Type, foreignKeyChecks: ForeignKeyChecks = .deferred) { + mutating func registerMigration( + _ storage: Storage?, + targetIdentifier: TargetMigrations.Identifier, + migration: Migration.Type, + foreignKeyChecks: ForeignKeyChecks = .deferred + ) { self.registerMigration( targetIdentifier.key(with: migration), - migrate: migration.loggedMigrate(targetIdentifier) + migrate: migration.loggedMigrate(storage, targetIdentifier: targetIdentifier) ) } } diff --git a/SessionUtilitiesKit/Utilities/ARC4RandomNumberGenerator.swift b/SessionUtilitiesKit/Utilities/ARC4RandomNumberGenerator.swift new file mode 100644 index 000000000..355b48a5f --- /dev/null +++ b/SessionUtilitiesKit/Utilities/ARC4RandomNumberGenerator.swift @@ -0,0 +1,73 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. +// +// Note: This was taken from TensorFlow's Random: +// https://github.com/apple/swift/blob/bc8f9e61d333b8f7a625f74d48ef0b554726e349/stdlib/public/TensorFlow/Random.swift +// +// the complex approach is needed due to an issue with Swift's randomElement(using:) +// generation (see https://stackoverflow.com/a/64897775 for more info) + +import Foundation + +public struct ARC4RandomNumberGenerator: RandomNumberGenerator { + var state: [UInt8] = Array(0...255) + var iPos: UInt8 = 0 + var jPos: UInt8 = 0 + + public init(seed: T) { + self.init( + seed: (0..<(UInt64.bitWidth / UInt64.bitWidth)).map { index in + UInt8(truncatingIfNeeded: seed >> (UInt8.bitWidth * index)) + } + ) + } + + public init(seed: [UInt8]) { + precondition(seed.count > 0, "Length of seed must be positive") + precondition(seed.count <= 256, "Length of seed must be at most 256") + + // Note: Have to use a for loop instead of a 'forEach' otherwise + // it doesn't work properly (not sure why...) + var j: UInt8 = 0 + for i: UInt8 in 0...255 { + j &+= S(i) &+ seed[Int(i) % seed.count] + swapAt(i, j) + } + } + + /// Produce the next random UInt64 from the stream, and advance the internal state + public mutating func next() -> UInt64 { + // Note: Have to use a for loop instead of a 'forEach' otherwise + // it doesn't work properly (not sure why...) + var result: UInt64 = 0 + for _ in 0.. UInt8 { + return state[Int(index)] + } + + /// Helper to swap elements of the state + private mutating func swapAt(_ i: UInt8, _ j: UInt8) { + state.swapAt(Int(i), Int(j)) + } + + /// Generates the next byte in the keystream. + private mutating func nextByte() -> UInt8 { + iPos &+= 1 + jPos &+= S(iPos) + swapAt(iPos, jPos) + return S(S(iPos) &+ S(jPos)) + } +} + +public extension ARC4RandomNumberGenerator { + mutating func nextBytes(count: Int) -> [UInt8] { + (0..