mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Fixed a number of bugs found during internal testing
Updated to the latest libSession to increase the available size for config message content (size check now happens after compression rather than before) Added some additional logs for config size info Fixed a bug where the database could be accessed before the migrations ran which could result in unexpected behaviours Fixed a bug where you couldn't mark a non one-to-one thread as read/unread Fixed a bug where a database initialization failure wouldn't result in a migration failure (user would be stuck on the splash screen indefinitely) Fixed a bug where if a message was too large for the screen the conversation would open it centered on the screen (now it will be positioned to the top) Started looking at broken unit tests Increased the build number
This commit is contained in:
parent
d2c82cb915
commit
f07313c7ac
|
@ -1 +1 @@
|
|||
Subproject commit 9777b37e8545febcc082578341352dba7433db21
|
||||
Subproject commit 49c78682a6f4546c8773113f3e201244f0b1e65a
|
|
@ -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 = "<group>"; };
|
||||
B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = "<group>"; };
|
||||
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = "<group>"; };
|
||||
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = "<group>"; };
|
||||
B897621B25D201F7004F83B2 /* RoundIconButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundIconButton.swift; sourceTree = "<group>"; };
|
||||
B8B320B6258C30D70020074B /* HTMLMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadata.swift; sourceTree = "<group>"; };
|
||||
B8B558F026C4BB0600693325 /* CameraManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraManager.swift; sourceTree = "<group>"; };
|
||||
B8B558FE26C4E05E00693325 /* WebRTCSession+MessageHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebRTCSession+MessageHandling.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1795,6 +1797,7 @@
|
|||
FD5C7304284F0FF30029977D /* MessageReceiver+VisibleMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+VisibleMessages.swift"; sourceTree = "<group>"; };
|
||||
FD5C7306284F103B0029977D /* MessageReceiver+MessageRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+MessageRequests.swift"; sourceTree = "<group>"; };
|
||||
FD5C7308285007920029977D /* BlindedIdLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdLookup.swift; sourceTree = "<group>"; };
|
||||
FD5CE3442A3C5D96001A6DE3 /* DecryptExportedKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecryptExportedKey.swift; sourceTree = "<group>"; };
|
||||
FD5D201D27B0D87C00FEA984 /* SessionId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionId.swift; sourceTree = "<group>"; };
|
||||
FD6A7A682818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetrieveDefaultOpenGroupRoomsJob.swift; sourceTree = "<group>"; };
|
||||
FD6A7A6A2818C17C00035AC1 /* UpdateProfilePictureJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateProfilePictureJob.swift; sourceTree = "<group>"; };
|
||||
|
@ -1881,6 +1884,8 @@
|
|||
FD8ECF912938552800C0D1BB /* Threading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = "<group>"; };
|
||||
FD8ECF93293856AF00C0D1BB /* Randomness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Randomness.swift; sourceTree = "<group>"; };
|
||||
FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = "<group>"; };
|
||||
FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARC4RandomNumberGenerator.swift; sourceTree = "<group>"; };
|
||||
FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnreadMarkerCell.swift; sourceTree = "<group>"; };
|
||||
FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = "<group>"; };
|
||||
FDA1E83529A5748F00C5C3BD /* ConfigUserGroupsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigUserGroupsSpec.swift; sourceTree = "<group>"; };
|
||||
FDA1E83829A5771A00C5C3BD /* LibSessionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionSpec.swift; sourceTree = "<group>"; };
|
||||
|
@ -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 = "<group>";
|
||||
|
@ -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 = "<group>";
|
||||
};
|
||||
FD16AB5D2A1DD8E70083D849 /* Profile Pictures */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
path = "Profile Pictures";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD17D79427F3E03300122BE0 /* Migrations */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -4289,6 +4288,7 @@
|
|||
children = (
|
||||
FDE7214F287E50D50093DF33 /* ProtoWrappers.py */,
|
||||
FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */,
|
||||
FD5CE3442A3C5D96001A6DE3 /* DecryptExportedKey.swift */,
|
||||
);
|
||||
path = Scripts;
|
||||
sourceTree = "<group>";
|
||||
|
@ -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)",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1651,11 +1631,19 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
|||
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 {
|
||||
|
|
|
@ -9,6 +9,13 @@ import SessionUtilitiesKit
|
|||
public class ConversationViewModel: OWSAudioPlayerDelegate {
|
||||
public typealias SectionModel = ArraySection<Section, MessageViewModel>
|
||||
|
||||
// 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,
|
||||
|
|
|
@ -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:
|
||||
|
|
73
Session/Conversations/Message Cells/UnreadMarkerCell.swift
Normal file
73
Session/Conversations/Message Cells/UnreadMarkerCell.swift
Normal file
|
@ -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<NSString, AnyObject>,
|
||||
playbackInfo: ConversationViewModel.PlaybackInfo?,
|
||||
showExpandedReactions: Bool,
|
||||
lastSearchText: String?
|
||||
) {
|
||||
guard cellViewModel.cellType == .unreadMarker else { return }
|
||||
}
|
||||
|
||||
override func dynamicUpdate(with cellViewModel: MessageViewModel, playbackInfo: ConversationViewModel.PlaybackInfo?) {}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 %@.";
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<T: BinaryInteger>(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..<UInt64.bitWidth / UInt8.bitWidth {
|
||||
result <<= UInt8.bitWidth
|
||||
result += UInt64(nextByte())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/// Helper to access the state
|
||||
private func S(_ index: UInt8) -> 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..<groupNameLength)
|
||||
|
@ -222,7 +161,7 @@ enum MockDataGenerator {
|
|||
logProgress("Closed Group Thread \(threadIndex)", "Generate \(numGroupMembers) Contacts")
|
||||
|
||||
(0..<numGroupMembers).forEach { _ in
|
||||
let contactData = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &cgThreadRandomGenerator) })
|
||||
let contactData: Data = Data(cgThreadRandomGenerator.nextBytes(count: 16))
|
||||
let randomSessionId: String = try! Identity.generate(from: contactData).x25519KeyPair.hexEncodedPublicKey
|
||||
let contactNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
|
||||
|
@ -353,7 +292,7 @@ enum MockDataGenerator {
|
|||
logProgress("Open Group Thread \(threadIndex)", "Generate \(numGroupMembers) Contacts")
|
||||
|
||||
(0..<numGroupMembers).forEach { _ in
|
||||
let contactData = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &ogThreadRandomGenerator) })
|
||||
let contactData: Data = Data(ogThreadRandomGenerator.nextBytes(count: 16))
|
||||
let randomSessionId: String = try! Identity.generate(from: contactData).x25519KeyPair.hexEncodedPublicKey
|
||||
let contactNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||
_ = try! Contact(
|
||||
|
|
|
@ -83,7 +83,7 @@ internal extension SessionUtil {
|
|||
}
|
||||
}
|
||||
catch {
|
||||
SNLog("[libSession] Failed to update/dump updated \(variant) config data")
|
||||
SNLog("[libSession] Failed to update/dump updated \(variant) config data due to error: \(error)")
|
||||
throw error
|
||||
}
|
||||
|
||||
|
|
|
@ -270,7 +270,9 @@ public enum SessionUtil {
|
|||
|
||||
var dumpResult: UnsafeMutablePointer<UInt8>? = nil
|
||||
var dumpResultLen: Int = 0
|
||||
config_dump(conf, &dumpResult, &dumpResultLen)
|
||||
try CExceptionHelper.performSafely {
|
||||
config_dump(conf, &dumpResult, &dumpResultLen)
|
||||
}
|
||||
|
||||
guard let dumpResult: UnsafeMutablePointer<UInt8> = 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_data> = config_push(conf)
|
||||
var cPushData: UnsafeMutablePointer<config_push_data>!
|
||||
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(
|
||||
|
|
|
@ -55,6 +55,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
|
|||
case genericAttachment
|
||||
case typingIndicator
|
||||
case dateHeader
|
||||
case unreadMarker
|
||||
}
|
||||
|
||||
public var differenceIdentifier: Int64 { id }
|
||||
|
|
|
@ -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<config_object>?,
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
]
|
||||
}
|
||||
|
|
|
@ -204,6 +204,9 @@ public indirect enum ThemeValue: Hashable {
|
|||
case profileIcon
|
||||
case profileIcon_greenPrimaryColor
|
||||
case profileIcon_background
|
||||
|
||||
// Unread Marker
|
||||
case unreadMarker
|
||||
}
|
||||
|
||||
// MARK: - ForcedThemeValue
|
||||
|
|
|
@ -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<Bool> = Atomic(false)
|
||||
private static let internalHasCreatedValidInstance: Atomic<Bool> = 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<Void, Error>, 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
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import Foundation
|
|||
public enum StorageError: Error {
|
||||
case generic
|
||||
case databaseInvalid
|
||||
case startupFailed
|
||||
case migrationFailed
|
||||
case invalidKeySpec
|
||||
case decodingFailed
|
||||
|
|
|
@ -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))")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<T: BinaryInteger>(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..<UInt64.bitWidth / UInt8.bitWidth {
|
||||
result <<= UInt8.bitWidth
|
||||
result += UInt64(nextByte())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/// Helper to access the state
|
||||
private func S(_ index: UInt8) -> 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..<count).map { _ in nextByte() }
|
||||
}
|
||||
}
|
|
@ -340,7 +340,8 @@ class PersistableRecordUtilitiesSpec: QuickSpec {
|
|||
beforeEach {
|
||||
var migrator: DatabaseMigrator = DatabaseMigrator()
|
||||
migrator.registerMigration(
|
||||
TestAddColumnMigration.target,
|
||||
mockStorage,
|
||||
targetIdentifier: TestAddColumnMigration.target,
|
||||
migration: TestAddColumnMigration.self
|
||||
)
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ open class ScreenLockViewController: UIViewController {
|
|||
open override func loadView() {
|
||||
super.loadView()
|
||||
|
||||
view.themeBackgroundColor = .black // Need to match the Launch screen
|
||||
view.themeBackgroundColorForced = .theme(.classicDark, color: .black) // Need to match the Launch screen
|
||||
|
||||
let edgesView: UIView = UIView.container()
|
||||
self.view.addSubview(edgesView)
|
||||
|
|
Loading…
Reference in a new issue