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