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 */; };
|
B88FA7FB26114EA70049422F /* Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88FA7FA26114EA70049422F /* Hex.swift */; };
|
||||||
B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; };
|
B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; };
|
||||||
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.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 */; };
|
B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B320B6258C30D70020074B /* HTMLMetadata.swift */; };
|
||||||
B8B558F126C4BB0600693325 /* CameraManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B558F026C4BB0600693325 /* CameraManager.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 */; };
|
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 */; };
|
FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */; };
|
||||||
FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7432804EF1B004C14C5 /* JobRunner.swift */; };
|
FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7432804EF1B004C14C5 /* JobRunner.swift */; };
|
||||||
FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F68280F9EDA002CFC5D /* JobRunnerError.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 */; };
|
FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */; };
|
||||||
FDA1E83629A5748F00C5C3BD /* ConfigUserGroupsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1E83529A5748F00C5C3BD /* ConfigUserGroupsSpec.swift */; };
|
FDA1E83629A5748F00C5C3BD /* ConfigUserGroupsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1E83529A5748F00C5C3BD /* ConfigUserGroupsSpec.swift */; };
|
||||||
FDA1E83929A5771A00C5C3BD /* LibSessionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1E83829A5771A00C5C3BD /* LibSessionSpec.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
FDA1E83829A5771A00C5C3BD /* LibSessionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionSpec.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2483,7 +2488,7 @@
|
||||||
B821493625D4D6A7009C0F2A /* Views & Modals */ = {
|
B821493625D4D6A7009C0F2A /* Views & Modals */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */,
|
B897621B25D201F7004F83B2 /* RoundIconButton.swift */,
|
||||||
B82149C025D605C6009C0F2A /* InfoBanner.swift */,
|
B82149C025D605C6009C0F2A /* InfoBanner.swift */,
|
||||||
C374EEEA25DA3CA70073A857 /* ConversationTitleView.swift */,
|
C374EEEA25DA3CA70073A857 /* ConversationTitleView.swift */,
|
||||||
FD4B200D283492210034334B /* InsetLockableTableView.swift */,
|
FD4B200D283492210034334B /* InsetLockableTableView.swift */,
|
||||||
|
@ -2519,6 +2524,7 @@
|
||||||
7B0EFDEF275084AA00FFAAE7 /* CallMessageCell.swift */,
|
7B0EFDEF275084AA00FFAAE7 /* CallMessageCell.swift */,
|
||||||
B8041AA625C90927003C2166 /* TypingIndicatorCell.swift */,
|
B8041AA625C90927003C2166 /* TypingIndicatorCell.swift */,
|
||||||
FDB7400C28EBEC240094D718 /* DateHeaderCell.swift */,
|
FDB7400C28EBEC240094D718 /* DateHeaderCell.swift */,
|
||||||
|
FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */,
|
||||||
);
|
);
|
||||||
path = "Message Cells";
|
path = "Message Cells";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2958,7 +2964,6 @@
|
||||||
children = (
|
children = (
|
||||||
C33FD9B7255A54A300E217F9 /* Meta */,
|
C33FD9B7255A54A300E217F9 /* Meta */,
|
||||||
C36096ED25AD20FD008B62B2 /* Media Viewing & Editing */,
|
C36096ED25AD20FD008B62B2 /* Media Viewing & Editing */,
|
||||||
FD16AB5D2A1DD8E70083D849 /* Profile Pictures */,
|
|
||||||
C36096EE25AD21BC008B62B2 /* Screen Lock */,
|
C36096EE25AD21BC008B62B2 /* Screen Lock */,
|
||||||
C3851CD225624B060061EEB0 /* Shared Views */,
|
C3851CD225624B060061EEB0 /* Shared Views */,
|
||||||
C360970125AD22D3008B62B2 /* Shared View Controllers */,
|
C360970125AD22D3008B62B2 /* Shared View Controllers */,
|
||||||
|
@ -3592,6 +3597,7 @@
|
||||||
FD09796527F6B0A800936362 /* Utilities */ = {
|
FD09796527F6B0A800936362 /* Utilities */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */,
|
||||||
FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */,
|
FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */,
|
||||||
FD3003692A3ADD6000B5A5FB /* CExceptionHelper.h */,
|
FD3003692A3ADD6000B5A5FB /* CExceptionHelper.h */,
|
||||||
FD30036D2A3AE26000B5A5FB /* CExceptionHelper.mm */,
|
FD30036D2A3AE26000B5A5FB /* CExceptionHelper.mm */,
|
||||||
|
@ -3635,13 +3641,6 @@
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
FD16AB5D2A1DD8E70083D849 /* Profile Pictures */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
);
|
|
||||||
path = "Profile Pictures";
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
FD17D79427F3E03300122BE0 /* Migrations */ = {
|
FD17D79427F3E03300122BE0 /* Migrations */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -4289,6 +4288,7 @@
|
||||||
children = (
|
children = (
|
||||||
FDE7214F287E50D50093DF33 /* ProtoWrappers.py */,
|
FDE7214F287E50D50093DF33 /* ProtoWrappers.py */,
|
||||||
FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */,
|
FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */,
|
||||||
|
FD5CE3442A3C5D96001A6DE3 /* DecryptExportedKey.swift */,
|
||||||
);
|
);
|
||||||
path = Scripts;
|
path = Scripts;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -5645,6 +5645,7 @@
|
||||||
FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */,
|
FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */,
|
||||||
C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */,
|
C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */,
|
||||||
C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */,
|
C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */,
|
||||||
|
FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */,
|
||||||
FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */,
|
FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */,
|
||||||
FD30036E2A3AE26000B5A5FB /* CExceptionHelper.mm in Sources */,
|
FD30036E2A3AE26000B5A5FB /* CExceptionHelper.mm in Sources */,
|
||||||
C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */,
|
C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */,
|
||||||
|
@ -5920,6 +5921,7 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
FD97B2422A3FEBF30027DD57 /* UnreadMarkerCell.swift in Sources */,
|
||||||
FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */,
|
FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */,
|
||||||
7B2561C429874851005C086C /* SessionCarouselView+Info.swift in Sources */,
|
7B2561C429874851005C086C /* SessionCarouselView+Info.swift in Sources */,
|
||||||
FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */,
|
FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */,
|
||||||
|
@ -6103,7 +6105,7 @@
|
||||||
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */,
|
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */,
|
||||||
FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */,
|
FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */,
|
||||||
FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */,
|
FDD2506E283711D600198BDA /* DifferenceKit+Utilities.swift in Sources */,
|
||||||
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */,
|
B897621C25D201F7004F83B2 /* RoundIconButton.swift in Sources */,
|
||||||
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */,
|
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */,
|
||||||
FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */,
|
FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */,
|
||||||
C302093E25DCBF08001F572D /* MentionSelectionView.swift in Sources */,
|
C302093E25DCBF08001F572D /* MentionSelectionView.swift in Sources */,
|
||||||
|
@ -6393,7 +6395,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 407;
|
CURRENT_PROJECT_VERSION = 408;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||||
|
@ -6465,7 +6467,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 407;
|
CURRENT_PROJECT_VERSION = 408;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
@ -6530,7 +6532,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 407;
|
CURRENT_PROJECT_VERSION = 408;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||||
|
@ -6604,7 +6606,7 @@
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 407;
|
CURRENT_PROJECT_VERSION = 408;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
@ -7512,7 +7514,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CURRENT_PROJECT_VERSION = 407;
|
CURRENT_PROJECT_VERSION = 408;
|
||||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
@ -7583,7 +7585,7 @@
|
||||||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CURRENT_PROJECT_VERSION = 407;
|
CURRENT_PROJECT_VERSION = 408;
|
||||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
FRAMEWORK_SEARCH_PATHS = (
|
FRAMEWORK_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
|
|
@ -16,7 +16,6 @@ extension ConversationVC:
|
||||||
InputViewDelegate,
|
InputViewDelegate,
|
||||||
MessageCellDelegate,
|
MessageCellDelegate,
|
||||||
ContextMenuActionDelegate,
|
ContextMenuActionDelegate,
|
||||||
ScrollToBottomButtonDelegate,
|
|
||||||
SendMediaNavDelegate,
|
SendMediaNavDelegate,
|
||||||
UIDocumentPickerDelegate,
|
UIDocumentPickerDelegate,
|
||||||
AttachmentApprovalViewControllerDelegate,
|
AttachmentApprovalViewControllerDelegate,
|
||||||
|
@ -51,15 +50,6 @@ extension ConversationVC:
|
||||||
navigationController?.pushViewController(viewController, animated: true)
|
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
|
// MARK: - Call
|
||||||
|
|
||||||
@objc func startCall(_ sender: Any?) {
|
@objc func startCall(_ sender: Any?) {
|
||||||
|
@ -858,10 +848,7 @@ extension ConversationVC:
|
||||||
|
|
||||||
UIView.animate(
|
UIView.animate(
|
||||||
withDuration: 0.25,
|
withDuration: 0.25,
|
||||||
animations: {
|
animations: { self?.updateScrollToBottom() },
|
||||||
self?.scrollButton.alpha = (self?.getScrollButtonOpacity() ?? 0)
|
|
||||||
self?.unreadCountView.alpha = (self?.scrollButton.alpha ?? 0)
|
|
||||||
},
|
|
||||||
completion: { _ in
|
completion: { _ in
|
||||||
guard let contentOffset: CGPoint = self?.tableView.contentOffset else { return }
|
guard let contentOffset: CGPoint = self?.tableView.contentOffset else { return }
|
||||||
|
|
||||||
|
@ -1052,7 +1039,7 @@ extension ConversationVC:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.scrollToInteractionIfNeeded(with: interactionInfo, highlight: true)
|
self.scrollToInteractionIfNeeded(with: interactionInfo, focusBehaviour: .highlight)
|
||||||
}
|
}
|
||||||
else if let linkPreview: LinkPreview = cellViewModel.linkPreview {
|
else if let linkPreview: LinkPreview = cellViewModel.linkPreview {
|
||||||
switch linkPreview.variant {
|
switch linkPreview.variant {
|
||||||
|
@ -1725,7 +1712,7 @@ extension ConversationVC:
|
||||||
|
|
||||||
func copy(_ cellViewModel: MessageViewModel) {
|
func copy(_ cellViewModel: MessageViewModel) {
|
||||||
switch cellViewModel.cellType {
|
switch cellViewModel.cellType {
|
||||||
case .typingIndicator, .dateHeader: break
|
case .typingIndicator, .dateHeader, .unreadMarker: break
|
||||||
|
|
||||||
case .textOnlyMessage:
|
case .textOnlyMessage:
|
||||||
if cellViewModel.body == nil, let linkPreview: LinkPreview = cellViewModel.linkPreview {
|
if cellViewModel.body == nil, let linkPreview: LinkPreview = cellViewModel.linkPreview {
|
||||||
|
|
|
@ -26,6 +26,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
private var hasReloadedThreadDataAfterDisappearance: Bool = true
|
private var hasReloadedThreadDataAfterDisappearance: Bool = true
|
||||||
|
|
||||||
var focusedInteractionInfo: Interaction.TimestampInfo?
|
var focusedInteractionInfo: Interaction.TimestampInfo?
|
||||||
|
var focusBehaviour: ConversationViewModel.FocusBehaviour = .none
|
||||||
var shouldHighlightNextScrollToInteraction: Bool = false
|
var shouldHighlightNextScrollToInteraction: Bool = false
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
|
@ -157,6 +158,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
)
|
)
|
||||||
result.registerHeaderFooterView(view: UITableViewHeaderFooterView.self)
|
result.registerHeaderFooterView(view: UITableViewHeaderFooterView.self)
|
||||||
result.register(view: DateHeaderCell.self)
|
result.register(view: DateHeaderCell.self)
|
||||||
|
result.register(view: UnreadMarkerCell.self)
|
||||||
result.register(view: VisibleMessageCell.self)
|
result.register(view: VisibleMessageCell.self)
|
||||||
result.register(view: InfoMessageCell.self)
|
result.register(view: InfoMessageCell.self)
|
||||||
result.register(view: TypingIndicatorCell.self)
|
result.register(view: TypingIndicatorCell.self)
|
||||||
|
@ -182,6 +184,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
result.set(.width, greaterThanOrEqualTo: ConversationVC.unreadCountViewSize)
|
result.set(.width, greaterThanOrEqualTo: ConversationVC.unreadCountViewSize)
|
||||||
result.set(.height, to: ConversationVC.unreadCountViewSize)
|
result.set(.height, to: ConversationVC.unreadCountViewSize)
|
||||||
result.isHidden = true
|
result.isHidden = true
|
||||||
|
result.alpha = 0
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}()
|
}()
|
||||||
|
@ -253,7 +256,20 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
return result
|
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 = {
|
lazy var messageRequestBackgroundView: UIView = {
|
||||||
let result: UIView = UIView()
|
let result: UIView = UIView()
|
||||||
|
@ -936,7 +952,6 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
.firstIndex(where: { item -> Bool in
|
.firstIndex(where: { item -> Bool in
|
||||||
// Since the first item is probably a `DateHeaderCell` (which would likely
|
// Since the first item is probably a `DateHeaderCell` (which would likely
|
||||||
// be removed when inserting items above it) we check if the id matches
|
// be removed when inserting items above it) we check if the id matches
|
||||||
// either the first or second item
|
|
||||||
let messages: [MessageViewModel] = self.viewModel
|
let messages: [MessageViewModel] = self.viewModel
|
||||||
.interactionData[oldSectionIndex]
|
.interactionData[oldSectionIndex]
|
||||||
.elements
|
.elements
|
||||||
|
@ -992,8 +1007,8 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
self?.searchController.resultsBar.stopLoading()
|
self?.searchController.resultsBar.stopLoading()
|
||||||
self?.scrollToInteractionIfNeeded(
|
self?.scrollToInteractionIfNeeded(
|
||||||
with: focusedInteractionInfo,
|
with: focusedInteractionInfo,
|
||||||
isAnimated: true,
|
focusBehaviour: (self?.shouldHighlightNextScrollToInteraction == true ? .highlight : .none),
|
||||||
highlight: (self?.shouldHighlightNextScrollToInteraction == true)
|
isAnimated: true
|
||||||
)
|
)
|
||||||
|
|
||||||
if wasLoadingMore {
|
if wasLoadingMore {
|
||||||
|
@ -1020,8 +1035,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Need to update the scroll button alpha in case new messages were added but we didn't scroll
|
// Need to update the scroll button alpha in case new messages were added but we didn't scroll
|
||||||
self.scrollButton.alpha = self.getScrollButtonOpacity()
|
self.updateScrollToBottom()
|
||||||
self.unreadCountView.alpha = self.scrollButton.alpha
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1070,8 +1084,8 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
self?.searchController.resultsBar.stopLoading()
|
self?.searchController.resultsBar.stopLoading()
|
||||||
self?.scrollToInteractionIfNeeded(
|
self?.scrollToInteractionIfNeeded(
|
||||||
with: focusedInteractionInfo,
|
with: focusedInteractionInfo,
|
||||||
isAnimated: true,
|
focusBehaviour: (self?.shouldHighlightNextScrollToInteraction == true ? .highlight : .none),
|
||||||
highlight: (self?.shouldHighlightNextScrollToInteraction == true)
|
isAnimated: true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1090,8 +1104,8 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
self?.searchController.resultsBar.stopLoading()
|
self?.searchController.resultsBar.stopLoading()
|
||||||
self?.scrollToInteractionIfNeeded(
|
self?.scrollToInteractionIfNeeded(
|
||||||
with: focusedInteractionInfo,
|
with: focusedInteractionInfo,
|
||||||
isAnimated: true,
|
focusBehaviour: (self?.shouldHighlightNextScrollToInteraction == true ? .highlight : .none),
|
||||||
highlight: (self?.shouldHighlightNextScrollToInteraction == true)
|
isAnimated: true
|
||||||
)
|
)
|
||||||
|
|
||||||
// Complete page loading
|
// 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,
|
// 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
|
// the screen will scroll to the bottom instead of the first unread message
|
||||||
if let focusedInteractionInfo: Interaction.TimestampInfo = self.viewModel.focusedInteractionInfo {
|
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 {
|
else {
|
||||||
self.scrollToBottom(isAnimated: false)
|
self.scrollToBottom(isAnimated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.scrollButton.alpha = self.getScrollButtonOpacity()
|
self.updateScrollToBottom()
|
||||||
self.unreadCountView.alpha = self.scrollButton.alpha
|
|
||||||
self.hasPerformedInitialScroll = true
|
self.hasPerformedInitialScroll = true
|
||||||
|
|
||||||
// Now that the data has loaded we need to check if either of the "load more" sections are
|
// 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?.messageRequestsViewBotomConstraint?.constant = -(keyboardTop + 12)
|
||||||
self?.tableView.contentInset = newContentInset
|
self?.tableView.contentInset = newContentInset
|
||||||
self?.tableView.contentOffset.y = newContentOffsetY
|
self?.tableView.contentOffset.y = newContentOffsetY
|
||||||
|
self?.updateScrollToBottom()
|
||||||
let scrollButtonOpacity: CGFloat = (self?.getScrollButtonOpacity() ?? 0)
|
|
||||||
self?.scrollButton.alpha = scrollButtonOpacity
|
|
||||||
self?.unreadCountView.alpha = scrollButtonOpacity
|
|
||||||
|
|
||||||
self?.view.setNeedsLayout()
|
self?.view.setNeedsLayout()
|
||||||
self?.view.layoutIfNeeded()
|
self?.view.layoutIfNeeded()
|
||||||
|
@ -1372,10 +1386,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
animations: { [weak self] in
|
animations: { [weak self] in
|
||||||
self?.scrollButtonBottomConstraint?.constant = -(keyboardTop + 12)
|
self?.scrollButtonBottomConstraint?.constant = -(keyboardTop + 12)
|
||||||
self?.messageRequestsViewBotomConstraint?.constant = -(keyboardTop + 12)
|
self?.messageRequestsViewBotomConstraint?.constant = -(keyboardTop + 12)
|
||||||
|
self?.updateScrollToBottom()
|
||||||
let scrollButtonOpacity: CGFloat = (self?.getScrollButtonOpacity() ?? 0)
|
|
||||||
self?.scrollButton.alpha = scrollButtonOpacity
|
|
||||||
self?.unreadCountView.alpha = scrollButtonOpacity
|
|
||||||
|
|
||||||
self?.view.setNeedsLayout()
|
self?.view.setNeedsLayout()
|
||||||
self?.view.layoutIfNeeded()
|
self?.view.layoutIfNeeded()
|
||||||
|
@ -1584,48 +1595,13 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||||
self.scrollButton.alpha = self.getScrollButtonOpacity()
|
self.updateScrollToBottom()
|
||||||
self.unreadCountView.alpha = self.scrollButton.alpha
|
|
||||||
|
|
||||||
// The initial scroll can trigger this logic but we already mark the initially focused message
|
// 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
|
// as read so don't run the below until the user actually scrolls after the initial layout
|
||||||
guard self.didFinishInitialLayout else { return }
|
guard self.didFinishInitialLayout else { return }
|
||||||
|
|
||||||
// We want to mark messages as read while we scroll, so grab the newest message and mark
|
self.markFullyVisibleAndOlderCellsAsRead(interactionInfo: nil)
|
||||||
// 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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||||
|
@ -1634,12 +1610,16 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
self.shouldHighlightNextScrollToInteraction
|
self.shouldHighlightNextScrollToInteraction
|
||||||
else {
|
else {
|
||||||
self.focusedInteractionInfo = nil
|
self.focusedInteractionInfo = nil
|
||||||
|
self.focusBehaviour = .none
|
||||||
self.shouldHighlightNextScrollToInteraction = false
|
self.shouldHighlightNextScrollToInteraction = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let behaviour: ConversationViewModel.FocusBehaviour = self.focusBehaviour
|
||||||
|
|
||||||
DispatchQueue.main.async { [weak self] in
|
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)
|
unreadCountView.isHidden = (unreadCount == 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getScrollButtonOpacity() -> CGFloat {
|
public func updateScrollToBottom() {
|
||||||
let contentOffsetY = tableView.contentOffset.y
|
// 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 x = (lastPageTop - ConversationVC.bottomInset - contentOffsetY).clamp(0, .greatestFiniteMagnitude)
|
||||||
let a = 1 / (ConversationVC.scrollButtonFullVisibilityThreshold - ConversationVC.scrollButtonNoVisibilityThreshold)
|
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
|
// MARK: - Search
|
||||||
|
@ -1769,23 +1757,19 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
}
|
}
|
||||||
|
|
||||||
func conversationSearchController(_ conversationSearchController: ConversationSearchController, didSelectInteractionInfo interactionInfo: Interaction.TimestampInfo) {
|
func conversationSearchController(_ conversationSearchController: ConversationSearchController, didSelectInteractionInfo interactionInfo: Interaction.TimestampInfo) {
|
||||||
scrollToInteractionIfNeeded(with: interactionInfo, highlight: true)
|
scrollToInteractionIfNeeded(with: interactionInfo, focusBehaviour: .highlight)
|
||||||
}
|
}
|
||||||
|
|
||||||
func scrollToInteractionIfNeeded(
|
func scrollToInteractionIfNeeded(
|
||||||
with interactionInfo: Interaction.TimestampInfo,
|
with interactionInfo: Interaction.TimestampInfo,
|
||||||
|
focusBehaviour: ConversationViewModel.FocusBehaviour = .none,
|
||||||
position: UITableView.ScrollPosition = .middle,
|
position: UITableView.ScrollPosition = .middle,
|
||||||
isJumpingToLastInteraction: Bool = false,
|
isJumpingToLastInteraction: Bool = false,
|
||||||
isAnimated: Bool = true,
|
isAnimated: Bool = true
|
||||||
highlight: Bool = false
|
|
||||||
) {
|
) {
|
||||||
// Store the info incase we need to load more data (call will be re-triggered)
|
// Store the info incase we need to load more data (call will be re-triggered)
|
||||||
self.focusedInteractionInfo = interactionInfo
|
self.focusedInteractionInfo = interactionInfo
|
||||||
self.shouldHighlightNextScrollToInteraction = highlight
|
self.shouldHighlightNextScrollToInteraction = (focusBehaviour == .highlight)
|
||||||
self.viewModel.markAsRead(
|
|
||||||
target: .threadAndInteractions(interactionsBeforeInclusive: interactionInfo.id),
|
|
||||||
timestampMs: interactionInfo.timestampMs
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure the target interaction has been loaded
|
// Ensure the target interaction has been loaded
|
||||||
guard
|
guard
|
||||||
|
@ -1819,16 +1803,47 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let targetIndexPath: IndexPath = IndexPath(
|
// 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,
|
row: targetMessageIndex,
|
||||||
section: messageSectionIndex
|
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
|
// 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(
|
self.tableView.scrollToRow(
|
||||||
at: targetIndexPath,
|
at: targetIndexPath,
|
||||||
at: position,
|
at: targetPosition,
|
||||||
animated: (self.didFinishInitialLayout && isAnimated)
|
animated: (self.didFinishInitialLayout && isAnimated)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1837,16 +1852,17 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
// of messages)
|
// of messages)
|
||||||
self.scrollViewDidScroll(self.tableView)
|
self.scrollViewDidScroll(self.tableView)
|
||||||
|
|
||||||
// If we haven't finished the initial layout then we want to delay the highlight slightly
|
// 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
|
// so it doesn't look buggy with the push transition and we know for sure the correct visible cells
|
||||||
if highlight {
|
// have been loaded
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(self.didFinishInitialLayout ? 0 : 150)) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(self.didFinishInitialLayout ? 0 : 150)) { [weak self] in
|
||||||
self?.highlightCellIfNeeded(interactionId: interactionInfo.id)
|
self?.markFullyVisibleAndOlderCellsAsRead(interactionInfo: interactionInfo)
|
||||||
}
|
self?.highlightCellIfNeeded(interactionId: interactionInfo.id, behaviour: focusBehaviour)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.shouldHighlightNextScrollToInteraction = false
|
self.shouldHighlightNextScrollToInteraction = false
|
||||||
self.focusedInteractionInfo = nil
|
self.focusedInteractionInfo = nil
|
||||||
|
self.focusBehaviour = .none
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1857,16 +1873,70 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
|
||||||
let targetRect: CGRect = self.tableView.rectForRow(at: targetIndexPath)
|
let targetRect: CGRect = self.tableView.rectForRow(at: targetIndexPath)
|
||||||
|
|
||||||
guard !self.tableView.bounds.contains(targetRect) else {
|
guard !self.tableView.bounds.contains(targetRect) else {
|
||||||
self.highlightCellIfNeeded(interactionId: interactionInfo.id)
|
self.markFullyVisibleAndOlderCellsAsRead(interactionInfo: interactionInfo)
|
||||||
|
self.highlightCellIfNeeded(interactionId: interactionInfo.id, behaviour: focusBehaviour)
|
||||||
return
|
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.shouldHighlightNextScrollToInteraction = false
|
||||||
self.focusedInteractionInfo = nil
|
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
|
// Trigger on the next run loop incase we are still finishing some other animation
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|
|
@ -9,6 +9,13 @@ import SessionUtilitiesKit
|
||||||
public class ConversationViewModel: OWSAudioPlayerDelegate {
|
public class ConversationViewModel: OWSAudioPlayerDelegate {
|
||||||
public typealias SectionModel = ArraySection<Section, MessageViewModel>
|
public typealias SectionModel = ArraySection<Section, MessageViewModel>
|
||||||
|
|
||||||
|
// MARK: - FocusBehaviour
|
||||||
|
|
||||||
|
public enum FocusBehaviour {
|
||||||
|
case none
|
||||||
|
case highlight
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Action
|
// MARK: - Action
|
||||||
|
|
||||||
public enum Action {
|
public enum Action {
|
||||||
|
@ -35,6 +42,8 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
||||||
public var sentMessageBeforeUpdate: Bool = false
|
public var sentMessageBeforeUpdate: Bool = false
|
||||||
public var lastSearchedText: String?
|
public var lastSearchedText: String?
|
||||||
public let focusedInteractionInfo: Interaction.TimestampInfo? // Note: This is used for global search
|
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 = {
|
public lazy var blockedBannerMessage: String = {
|
||||||
switch self.threadData.threadVariant {
|
switch self.threadData.threadVariant {
|
||||||
|
@ -116,6 +125,13 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
||||||
self.threadId = threadId
|
self.threadId = threadId
|
||||||
self.initialThreadVariant = threadVariant
|
self.initialThreadVariant = threadVariant
|
||||||
self.focusedInteractionInfo = initialData?.targetInteractionInfo
|
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(
|
self.threadData = SessionThreadViewModel(
|
||||||
threadId: threadId,
|
threadId: threadId,
|
||||||
threadVariant: threadVariant,
|
threadVariant: threadVariant,
|
||||||
|
@ -321,6 +337,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func process(data: [MessageViewModel], for pageInfo: PagedData.PageInfo) -> [SectionModel] {
|
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 typingIndicator: MessageViewModel? = data.first(where: { $0.isTypingIndicator == true })
|
||||||
let sortedData: [MessageViewModel] = data
|
let sortedData: [MessageViewModel] = data
|
||||||
.filter { $0.isTypingIndicator != true }
|
.filter { $0.isTypingIndicator != true }
|
||||||
|
@ -362,11 +379,20 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.reduce([]) { result, message in
|
.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 {
|
guard message.shouldShowDateHeader else {
|
||||||
return result.appending(message)
|
return updatedResult.appending(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return updatedResult
|
||||||
.appending(
|
.appending(
|
||||||
MessageViewModel(
|
MessageViewModel(
|
||||||
timestampMs: message.timestampMs,
|
timestampMs: message.timestampMs,
|
||||||
|
|
|
@ -65,6 +65,7 @@ public class MessageCell: UITableViewCell {
|
||||||
static func cellType(for viewModel: MessageViewModel) -> MessageCell.Type {
|
static func cellType(for viewModel: MessageViewModel) -> MessageCell.Type {
|
||||||
guard viewModel.cellType != .typingIndicator else { return TypingIndicatorCell.self }
|
guard viewModel.cellType != .typingIndicator else { return TypingIndicatorCell.self }
|
||||||
guard viewModel.cellType != .dateHeader else { return DateHeaderCell.self }
|
guard viewModel.cellType != .dateHeader else { return DateHeaderCell.self }
|
||||||
|
guard viewModel.cellType != .unreadMarker else { return UnreadMarkerCell.self }
|
||||||
|
|
||||||
switch viewModel.variant {
|
switch viewModel.variant {
|
||||||
case .standardOutgoing, .standardIncoming, .standardIncomingDeleted:
|
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 {
|
switch cellViewModel.cellType {
|
||||||
case .typingIndicator, .dateHeader: break
|
case .typingIndicator, .dateHeader, .unreadMarker: break
|
||||||
|
|
||||||
case .textOnlyMessage:
|
case .textOnlyMessage:
|
||||||
let inset: CGFloat = 12
|
let inset: CGFloat = 12
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import SessionUIKit
|
import SessionUIKit
|
||||||
|
|
||||||
final class ScrollToBottomButton: UIView {
|
final class RoundIconButton: UIView {
|
||||||
private weak var delegate: ScrollToBottomButtonDelegate?
|
private let onTap: () -> ()
|
||||||
|
|
||||||
// MARK: - Settings
|
// MARK: - Settings
|
||||||
|
|
||||||
|
@ -13,12 +13,12 @@ final class ScrollToBottomButton: UIView {
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
init(delegate: ScrollToBottomButtonDelegate) {
|
init(image: UIImage?, onTap: @escaping () -> ()) {
|
||||||
self.delegate = delegate
|
self.onTap = onTap
|
||||||
|
|
||||||
super.init(frame: CGRect.zero)
|
super.init(frame: CGRect.zero)
|
||||||
|
|
||||||
setUpViewHierarchy()
|
setUpViewHierarchy(image: image)
|
||||||
}
|
}
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
|
@ -29,7 +29,7 @@ final class ScrollToBottomButton: UIView {
|
||||||
preconditionFailure("Use init(delegate:) instead.")
|
preconditionFailure("Use init(delegate:) instead.")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setUpViewHierarchy() {
|
private func setUpViewHierarchy(image: UIImage?) {
|
||||||
// Background & blur
|
// Background & blur
|
||||||
let backgroundView = UIView()
|
let backgroundView = UIView()
|
||||||
backgroundView.themeBackgroundColor = .backgroundSecondary
|
backgroundView.themeBackgroundColor = .backgroundSecondary
|
||||||
|
@ -49,9 +49,9 @@ final class ScrollToBottomButton: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size & shape
|
// Size & shape
|
||||||
set(.width, to: ScrollToBottomButton.size)
|
set(.width, to: RoundIconButton.size)
|
||||||
set(.height, to: ScrollToBottomButton.size)
|
set(.height, to: RoundIconButton.size)
|
||||||
layer.cornerRadius = (ScrollToBottomButton.size / 2)
|
layer.cornerRadius = (RoundIconButton.size / 2)
|
||||||
layer.masksToBounds = true
|
layer.masksToBounds = true
|
||||||
|
|
||||||
// Border
|
// Border
|
||||||
|
@ -59,16 +59,13 @@ final class ScrollToBottomButton: UIView {
|
||||||
layer.borderWidth = Values.separatorThickness
|
layer.borderWidth = Values.separatorThickness
|
||||||
|
|
||||||
// Icon
|
// Icon
|
||||||
let iconImageView = UIImageView(
|
let iconImageView = UIImageView(image: image)
|
||||||
image: UIImage(named: "ic_chevron_down")?
|
|
||||||
.withRenderingMode(.alwaysTemplate)
|
|
||||||
)
|
|
||||||
iconImageView.themeTintColor = .textPrimary
|
iconImageView.themeTintColor = .textPrimary
|
||||||
iconImageView.contentMode = .scaleAspectFit
|
iconImageView.contentMode = .scaleAspectFit
|
||||||
addSubview(iconImageView)
|
addSubview(iconImageView)
|
||||||
iconImageView.center(in: self)
|
iconImageView.center(in: self)
|
||||||
iconImageView.set(.width, to: ScrollToBottomButton.iconSize)
|
iconImageView.set(.width, to: RoundIconButton.iconSize)
|
||||||
iconImageView.set(.height, to: ScrollToBottomButton.iconSize)
|
iconImageView.set(.height, to: RoundIconButton.iconSize)
|
||||||
|
|
||||||
// Gesture recognizer
|
// Gesture recognizer
|
||||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
||||||
|
@ -78,12 +75,6 @@ final class ScrollToBottomButton: UIView {
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
||||||
@objc private func handleTap() {
|
@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 {
|
switch section.model {
|
||||||
case .threads:
|
case .threads:
|
||||||
// Cannot properly sync outgoing blinded message requests so don't provide the option
|
// Cannot properly sync outgoing blinded message requests so don't provide the option
|
||||||
guard SessionId(from: section.elements[indexPath.row].threadId)?.prefix == .standard else {
|
guard
|
||||||
return nil
|
threadViewModel.threadVariant != .contact ||
|
||||||
}
|
SessionId(from: section.elements[indexPath.row].threadId)?.prefix == .standard
|
||||||
|
else { return nil }
|
||||||
|
|
||||||
return UIContextualAction.configuration(
|
return UIContextualAction.configuration(
|
||||||
for: UIContextualAction.generateSwipeActions(
|
for: UIContextualAction.generateSwipeActions(
|
||||||
|
|
|
@ -47,9 +47,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||||
let mainWindow: UIWindow = TraitObservingWindow(frame: UIScreen.main.bounds)
|
let mainWindow: UIWindow = TraitObservingWindow(frame: UIScreen.main.bounds)
|
||||||
self.loadingViewController = LoadingViewController()
|
self.loadingViewController = LoadingViewController()
|
||||||
|
|
||||||
// Store a weak reference in the ThemeManager so it can properly apply themes as needed
|
|
||||||
ThemeManager.mainWindow = mainWindow
|
|
||||||
|
|
||||||
AppSetup.setupEnvironment(
|
AppSetup.setupEnvironment(
|
||||||
appSpecificBlock: {
|
appSpecificBlock: {
|
||||||
// Create AppEnvironment
|
// Create AppEnvironment
|
||||||
|
@ -78,6 +75,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||||
return
|
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)
|
self?.completePostMigrationSetup(calledFrom: .finishLaunching, needsConfigSync: needsConfigSync)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -333,7 +335,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||||
private func showFailedMigrationAlert(calledFrom lifecycleMethod: LifecycleMethod, error: Error?) {
|
private func showFailedMigrationAlert(calledFrom lifecycleMethod: LifecycleMethod, error: Error?) {
|
||||||
let alert = UIAlertController(
|
let alert = UIAlertController(
|
||||||
title: "Session",
|
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
|
preferredStyle: .alert
|
||||||
)
|
)
|
||||||
alert.addAction(UIAlertAction(title: "HELP_REPORT_BUG_ACTION_TITLE".localized(), style: .default) { _ in
|
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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "متاسفانه خطایی رخ داده است";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "لطفا بعدا دوباره تلاش کنید";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "لطفا بعدا دوباره تلاش کنید";
|
||||||
"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" = "هنگام بهینهسازی پایگاه داده خطایی روی داد\n\nشما میتوانید گزارشهای برنامه خود را صادر کنید تا بتوانید برای عیبیابی به اشتراک بگذارید یا میتوانید دستگاه خود را بازیابی کنید\n\nهشدار: بازیابی دستگاه شما منجر به از دست رفتن دادههای قدیمیتر از دو هفته میشود.";
|
"DATABASE_MIGRATION_FAILED" = "هنگام بهینهسازی پایگاه داده خطایی روی داد\n\nشما میتوانید گزارشهای برنامه خود را صادر کنید تا بتوانید برای عیبیابی به اشتراک بگذارید یا میتوانید دستگاه خود را بازیابی کنید\n\nهشدار: بازیابی دستگاه شما منجر به از دست رفتن دادههای قدیمیتر از دو هفته میشود.";
|
||||||
"RECOVERY_PHASE_ERROR_GENERIC" = "مشکلی پیش آمد. لطفاً عبارت بازیابی خود را بررسی کنید و دوباره امتحان کنید.";
|
"RECOVERY_PHASE_ERROR_GENERIC" = "مشکلی پیش آمد. لطفاً عبارت بازیابی خود را بررسی کنید و دوباره امتحان کنید.";
|
||||||
"RECOVERY_PHASE_ERROR_LENGTH" = "به نظر می رسد کلمات کافی وارد نکرده اید. لطفاً عبارت بازیابی خود را بررسی کنید و دوباره امتحان کنید.";
|
"RECOVERY_PHASE_ERROR_LENGTH" = "به نظر می رسد کلمات کافی وارد نکرده اید. لطفاً عبارت بازیابی خود را بررسی کنید و دوباره امتحان کنید.";
|
||||||
|
@ -637,6 +638,7 @@
|
||||||
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oups, une erreur est survenue";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Veuillez réessayer plus tard";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Veuillez réessayer plus tard";
|
||||||
"LOADING_CONVERSATIONS" = "Chargement des conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have 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_TITLE" = "Oops, an error occurred";
|
||||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
"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";
|
"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_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.";
|
"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";
|
"CONTACT_NICKNAME_PLACEHOLDER" = "Enter a name";
|
||||||
"MARK_AS_READ" = "Mark Read";
|
"MARK_AS_READ" = "Mark Read";
|
||||||
"MARK_AS_UNREAD" = "Mark Unread";
|
"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" = "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_READ_ONLY" = "There are no messages in %@.";
|
||||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||||
|
|
|
@ -15,7 +15,7 @@ class ScreenLockUI {
|
||||||
result.isHidden = false
|
result.isHidden = false
|
||||||
result.windowLevel = ._Background
|
result.windowLevel = ._Background
|
||||||
result.isOpaque = true
|
result.isOpaque = true
|
||||||
result.themeBackgroundColor = .backgroundPrimary
|
result.themeBackgroundColorForced = .theme(.classicDark, color: .backgroundPrimary)
|
||||||
result.rootViewController = self.screenBlockingViewController
|
result.rootViewController = self.screenBlockingViewController
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -291,7 +291,7 @@ class ScreenLockUI {
|
||||||
window.isHidden = false
|
window.isHidden = false
|
||||||
window.windowLevel = ._Background
|
window.windowLevel = ._Background
|
||||||
window.isOpaque = true
|
window.isOpaque = true
|
||||||
window.themeBackgroundColor = .backgroundPrimary
|
window.themeBackgroundColorForced = .theme(.classicDark, color: .backgroundPrimary)
|
||||||
|
|
||||||
let viewController: ScreenLockViewController = ScreenLockViewController { [weak self] in
|
let viewController: ScreenLockViewController = ScreenLockViewController { [weak self] in
|
||||||
guard self?.appIsInactiveOrBackground == false else {
|
guard self?.appIsInactiveOrBackground == false else {
|
||||||
|
|
|
@ -6,67 +6,6 @@ import Curve25519Kit
|
||||||
import SessionMessagingKit
|
import SessionMessagingKit
|
||||||
|
|
||||||
enum MockDataGenerator {
|
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
|
// MARK: - Generation
|
||||||
|
|
||||||
static var printProgress: Bool = true
|
static var printProgress: Bool = true
|
||||||
|
@ -125,7 +64,7 @@ enum MockDataGenerator {
|
||||||
|
|
||||||
logProgress("DM Thread \(threadIndex)", "Start")
|
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 randomSessionId: String = try! Identity.generate(from: data).x25519KeyPair.hexEncodedPublicKey
|
||||||
let isMessageRequest: Bool = Bool.random(using: &dmThreadRandomGenerator)
|
let isMessageRequest: Bool = Bool.random(using: &dmThreadRandomGenerator)
|
||||||
let contactNameLength: Int = ((5..<20).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
let contactNameLength: Int = ((5..<20).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||||
|
@ -207,7 +146,7 @@ enum MockDataGenerator {
|
||||||
|
|
||||||
logProgress("Closed Group Thread \(threadIndex)", "Start")
|
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 randomGroupPublicKey: String = try! Identity.generate(from: data).x25519KeyPair.hexEncodedPublicKey
|
||||||
let groupNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
let groupNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||||
let groupName: String = (0..<groupNameLength)
|
let groupName: String = (0..<groupNameLength)
|
||||||
|
@ -222,7 +161,7 @@ enum MockDataGenerator {
|
||||||
logProgress("Closed Group Thread \(threadIndex)", "Generate \(numGroupMembers) Contacts")
|
logProgress("Closed Group Thread \(threadIndex)", "Generate \(numGroupMembers) Contacts")
|
||||||
|
|
||||||
(0..<numGroupMembers).forEach { _ in
|
(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 randomSessionId: String = try! Identity.generate(from: contactData).x25519KeyPair.hexEncodedPublicKey
|
||||||
let contactNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
let contactNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||||
|
|
||||||
|
@ -353,7 +292,7 @@ enum MockDataGenerator {
|
||||||
logProgress("Open Group Thread \(threadIndex)", "Generate \(numGroupMembers) Contacts")
|
logProgress("Open Group Thread \(threadIndex)", "Generate \(numGroupMembers) Contacts")
|
||||||
|
|
||||||
(0..<numGroupMembers).forEach { _ in
|
(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 randomSessionId: String = try! Identity.generate(from: contactData).x25519KeyPair.hexEncodedPublicKey
|
||||||
let contactNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
let contactNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||||
_ = try! Contact(
|
_ = try! Contact(
|
||||||
|
|
|
@ -83,7 +83,7 @@ internal extension SessionUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
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
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -270,7 +270,9 @@ public enum SessionUtil {
|
||||||
|
|
||||||
var dumpResult: UnsafeMutablePointer<UInt8>? = nil
|
var dumpResult: UnsafeMutablePointer<UInt8>? = nil
|
||||||
var dumpResultLen: Int = 0
|
var dumpResultLen: Int = 0
|
||||||
|
try CExceptionHelper.performSafely {
|
||||||
config_dump(conf, &dumpResult, &dumpResultLen)
|
config_dump(conf, &dumpResult, &dumpResultLen)
|
||||||
|
}
|
||||||
|
|
||||||
guard let dumpResult: UnsafeMutablePointer<UInt8> = dumpResult else { return nil }
|
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
|
// 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)
|
// data yet (to deal with first launch cases)
|
||||||
return existingDumpVariants
|
return try existingDumpVariants
|
||||||
.compactMap { variant -> OutgoingConfResult? in
|
.compactMap { variant -> OutgoingConfResult? in
|
||||||
SessionUtil
|
try SessionUtil
|
||||||
.config(for: variant, publicKey: publicKey)
|
.config(for: variant, publicKey: publicKey)
|
||||||
.mutate { conf in
|
.mutate { conf in
|
||||||
// Check if the config needs to be pushed
|
// Check if the config needs to be pushed
|
||||||
guard conf != nil && config_needs_push(conf) else { return nil }
|
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(
|
let pushData: Data = Data(
|
||||||
bytes: cPushData.pointee.config,
|
bytes: cPushData.pointee.config,
|
||||||
count: cPushData.pointee.config_len
|
count: cPushData.pointee.config_len
|
||||||
|
@ -328,6 +355,7 @@ public enum SessionUtil {
|
||||||
)
|
)
|
||||||
let seqNo: Int64 = cPushData.pointee.seqno
|
let seqNo: Int64 = cPushData.pointee.seqno
|
||||||
cPushData.deallocate()
|
cPushData.deallocate()
|
||||||
|
SNLog("[libSession - DEBUG] Push data for \(variant) config data, size: \(configCountInfo), bytes: \(pushData.count)")
|
||||||
|
|
||||||
return OutgoingConfResult(
|
return OutgoingConfResult(
|
||||||
message: SharedConfigMessage(
|
message: SharedConfigMessage(
|
||||||
|
|
|
@ -55,6 +55,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
|
||||||
case genericAttachment
|
case genericAttachment
|
||||||
case typingIndicator
|
case typingIndicator
|
||||||
case dateHeader
|
case dateHeader
|
||||||
|
case unreadMarker
|
||||||
}
|
}
|
||||||
|
|
||||||
public var differenceIdentifier: Int64 { id }
|
public var differenceIdentifier: Int64 { id }
|
||||||
|
|
|
@ -53,8 +53,15 @@ class ConfigContactsSpec {
|
||||||
|
|
||||||
// MARK: -- it can catch size limit errors thrown when pushing
|
// MARK: -- it can catch size limit errors thrown when pushing
|
||||||
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
|
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)
|
contacts_set(conf, &contact)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,12 +77,19 @@ class ConfigContactsSpec {
|
||||||
|
|
||||||
// MARK: -- can catch size limit errors thrown when dumping
|
// MARK: -- can catch size limit errors thrown when dumping
|
||||||
it("can catch size limit errors thrown when dumping") {
|
it("can catch size limit errors thrown when dumping") {
|
||||||
try (0..<10000).forEach { index in
|
var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000)
|
||||||
var contact: contacts_contact = try createContact(for: index, in: conf, maxing: .allProperties)
|
|
||||||
|
try (0..<100000).forEach { index in
|
||||||
|
var contact: contacts_contact = try createContact(
|
||||||
|
for: index,
|
||||||
|
in: conf,
|
||||||
|
rand: &randomGenerator,
|
||||||
|
maxing: .allProperties
|
||||||
|
)
|
||||||
contacts_set(conf, &contact)
|
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_push(conf)).to(beTrue())
|
||||||
expect(config_needs_dump(conf)).to(beTrue())
|
expect(config_needs_dump(conf)).to(beTrue())
|
||||||
|
|
||||||
|
@ -117,8 +131,14 @@ class ConfigContactsSpec {
|
||||||
|
|
||||||
// MARK: -- has not changed the max empty records
|
// MARK: -- has not changed the max empty records
|
||||||
it("has not changed the max empty records") {
|
it("has not changed the max empty records") {
|
||||||
for index in (0..<10000) {
|
var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000)
|
||||||
var contact: contacts_contact = try createContact(for: index, in: conf)
|
|
||||||
|
for index in (0..<100000) {
|
||||||
|
var contact: contacts_contact = try createContact(
|
||||||
|
for: index,
|
||||||
|
in: conf,
|
||||||
|
rand: &randomGenerator
|
||||||
|
)
|
||||||
contacts_set(conf, &contact)
|
contacts_set(conf, &contact)
|
||||||
|
|
||||||
do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
|
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
|
// 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
|
// MARK: -- has not changed the max name only records
|
||||||
it("has not changed the max name only records") {
|
it("has not changed the max name only records") {
|
||||||
for index in (0..<10000) {
|
var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000)
|
||||||
var contact: contacts_contact = try createContact(for: index, in: conf, maxing: [.name])
|
|
||||||
|
for index in (0..<100000) {
|
||||||
|
var contact: contacts_contact = try createContact(
|
||||||
|
for: index,
|
||||||
|
in: conf,
|
||||||
|
rand: &randomGenerator,
|
||||||
|
maxing: [.name]
|
||||||
|
)
|
||||||
contacts_set(conf, &contact)
|
contacts_set(conf, &contact)
|
||||||
|
|
||||||
do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
|
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
|
// 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
|
// MARK: -- has not changed the max name and profile pic only records
|
||||||
it("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 randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000)
|
||||||
var contact: contacts_contact = try createContact(for: index, in: conf, maxing: [.name, .profile_pic])
|
|
||||||
|
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)
|
contacts_set(conf, &contact)
|
||||||
|
|
||||||
do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
|
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
|
// 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
|
// MARK: -- has not changed the max filled records
|
||||||
it("has not changed the max filled records") {
|
it("has not changed the max filled records") {
|
||||||
for index in (0..<10000) {
|
var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000)
|
||||||
var contact: contacts_contact = try createContact(for: index, in: conf, maxing: .allProperties)
|
|
||||||
|
for index in (0..<100000) {
|
||||||
|
var contact: contacts_contact = try createContact(
|
||||||
|
for: index,
|
||||||
|
in: conf,
|
||||||
|
rand: &randomGenerator,
|
||||||
|
maxing: .allProperties
|
||||||
|
)
|
||||||
contacts_set(conf, &contact)
|
contacts_set(conf, &contact)
|
||||||
|
|
||||||
do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
|
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
|
// 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(
|
private static func createContact(
|
||||||
for index: Int,
|
for index: Int,
|
||||||
in conf: UnsafeMutablePointer<config_object>?,
|
in conf: UnsafeMutablePointer<config_object>?,
|
||||||
|
rand: inout ARC4RandomNumberGenerator,
|
||||||
maxing properties: [ContactProperty] = []
|
maxing properties: [ContactProperty] = []
|
||||||
) throws -> contacts_contact {
|
) 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))
|
let sessionId: String = ("05\(index)a" + postPrefixId.suffix(postPrefixId.count - "05\(index)a".count))
|
||||||
var cSessionId: [CChar] = sessionId.cArray.nullTerminated()
|
var cSessionId: [CChar] = sessionId.cArray.nullTerminated()
|
||||||
var contact: contacts_contact = contacts_contact()
|
var contact: contacts_contact = contacts_contact()
|
||||||
|
@ -569,33 +611,22 @@ class ConfigContactsSpec {
|
||||||
case .mute_until: contact.mute_until = Int64.max
|
case .mute_until: contact.mute_until = Int64.max
|
||||||
|
|
||||||
case .name:
|
case .name:
|
||||||
contact.name = String(
|
contact.name = rand.nextBytes(count: SessionUtil.libSessionMaxNameByteLength)
|
||||||
data: Data(
|
.toHexString()
|
||||||
repeating: "A".data(using: .utf8)![0],
|
.toLibSession()
|
||||||
count: SessionUtil.libSessionMaxNameByteLength
|
|
||||||
),
|
|
||||||
encoding: .utf8
|
|
||||||
).toLibSession()
|
|
||||||
|
|
||||||
case .nickname:
|
case .nickname:
|
||||||
contact.nickname = String(
|
contact.nickname = rand.nextBytes(count: SessionUtil.libSessionMaxNameByteLength)
|
||||||
data: Data(
|
.toHexString()
|
||||||
repeating: "A".data(using: .utf8)![0],
|
.toLibSession()
|
||||||
count: SessionUtil.libSessionMaxNameByteLength
|
|
||||||
),
|
|
||||||
encoding: .utf8
|
|
||||||
).toLibSession()
|
|
||||||
|
|
||||||
case .profile_pic:
|
case .profile_pic:
|
||||||
contact.profile_pic = user_profile_pic(
|
contact.profile_pic = user_profile_pic(
|
||||||
url: String(
|
url: rand.nextBytes(count: SessionUtil.libSessionMaxProfileUrlByteLength)
|
||||||
data: Data(
|
.toHexString()
|
||||||
repeating: "A".data(using: .utf8)![0],
|
.toLibSession(),
|
||||||
count: SessionUtil.libSessionMaxProfileUrlByteLength
|
key: Data(rand.nextBytes(count: 32))
|
||||||
),
|
.toLibSession()
|
||||||
encoding: .utf8
|
|
||||||
).toLibSession(),
|
|
||||||
key: "qwerty78901234567890123456789012".data(using: .utf8)!.toLibSession()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -407,9 +407,11 @@ internal class ThemeApplier {
|
||||||
.compactMap { $0?.clearingOtherAppliers() }
|
.compactMap { $0?.clearingOtherAppliers() }
|
||||||
.filter { $0.info != info }
|
.filter { $0.info != info }
|
||||||
|
|
||||||
// Automatically apply the theme immediately
|
// Automatically apply the theme immediately (if the database has been setup)
|
||||||
|
if Storage.hasCreatedValidInstance {
|
||||||
self.apply(theme: ThemeManager.currentTheme, isInitialApplication: true)
|
self.apply(theme: ThemeManager.currentTheme, isInitialApplication: true)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Functions
|
// MARK: - Functions
|
||||||
|
|
||||||
|
|
|
@ -115,6 +115,9 @@ internal enum Theme_ClassicDark: ThemeColors {
|
||||||
// Profile
|
// Profile
|
||||||
.profileIcon: .primary,
|
.profileIcon: .primary,
|
||||||
.profileIcon_greenPrimaryColor: .black,
|
.profileIcon_greenPrimaryColor: .black,
|
||||||
.profileIcon_background: .white
|
.profileIcon_background: .white,
|
||||||
|
|
||||||
|
// Unread Marker
|
||||||
|
.unreadMarker: .primary
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,6 +115,9 @@ internal enum Theme_ClassicLight: ThemeColors {
|
||||||
// Profile
|
// Profile
|
||||||
.profileIcon: .primary,
|
.profileIcon: .primary,
|
||||||
.profileIcon_greenPrimaryColor: .primary,
|
.profileIcon_greenPrimaryColor: .primary,
|
||||||
.profileIcon_background: .black
|
.profileIcon_background: .black,
|
||||||
|
|
||||||
|
// Unread Marker
|
||||||
|
.unreadMarker: .black
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,6 +115,9 @@ internal enum Theme_OceanDark: ThemeColors {
|
||||||
// Profile
|
// Profile
|
||||||
.profileIcon: .primary,
|
.profileIcon: .primary,
|
||||||
.profileIcon_greenPrimaryColor: .black,
|
.profileIcon_greenPrimaryColor: .black,
|
||||||
.profileIcon_background: .white
|
.profileIcon_background: .white,
|
||||||
|
|
||||||
|
// Unread Marker
|
||||||
|
.unreadMarker: .primary
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,6 +115,9 @@ internal enum Theme_OceanLight: ThemeColors {
|
||||||
// Profile
|
// Profile
|
||||||
.profileIcon: .primary,
|
.profileIcon: .primary,
|
||||||
.profileIcon_greenPrimaryColor: .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
|
||||||
case profileIcon_greenPrimaryColor
|
case profileIcon_greenPrimaryColor
|
||||||
case profileIcon_background
|
case profileIcon_background
|
||||||
|
|
||||||
|
// Unread Marker
|
||||||
|
case unreadMarker
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - ForcedThemeValue
|
// MARK: - ForcedThemeValue
|
||||||
|
|
|
@ -17,13 +17,16 @@ open class Storage {
|
||||||
private static var databasePathShm: String { "\(Storage.sharedDatabaseDirectoryPath)/\(Storage.dbFileName)-shm" }
|
private static var databasePathShm: String { "\(Storage.sharedDatabaseDirectoryPath)/\(Storage.dbFileName)-shm" }
|
||||||
private static var databasePathWal: String { "\(Storage.sharedDatabaseDirectoryPath)/\(Storage.dbFileName)-wal" }
|
private static var databasePathWal: String { "\(Storage.sharedDatabaseDirectoryPath)/\(Storage.dbFileName)-wal" }
|
||||||
|
|
||||||
|
public static var hasCreatedValidInstance: Bool { internalHasCreatedValidInstance.wrappedValue }
|
||||||
public static var isDatabasePasswordAccessible: Bool {
|
public static var isDatabasePasswordAccessible: Bool {
|
||||||
guard (try? getDatabaseCipherKeySpec()) != nil else { return false }
|
guard (try? getDatabaseCipherKeySpec()) != nil else { return false }
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var startupError: Error?
|
||||||
private let migrationsCompleted: Atomic<Bool> = Atomic(false)
|
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)
|
internal let internalCurrentlyRunningMigration: Atomic<(identifier: TargetMigrations.Identifier, migration: Migration.Type)?> = Atomic(nil)
|
||||||
|
|
||||||
public static let shared: Storage = Storage()
|
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)
|
// If a custom writer was provided then use that (for unit testing)
|
||||||
guard customWriter == nil else {
|
guard customWriter == nil else {
|
||||||
dbWriter = customWriter
|
dbWriter = customWriter
|
||||||
isValid = true
|
|
||||||
perform(migrations: (customMigrations ?? []), async: false, onProgressUpdate: nil, onComplete: { _, _ in })
|
perform(migrations: (customMigrations ?? []), async: false, onProgressUpdate: nil, onComplete: { _, _ in })
|
||||||
|
isValid = true
|
||||||
|
Storage.internalHasCreatedValidInstance.mutate { $0 = true }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,8 +103,9 @@ open class Storage {
|
||||||
configuration: config
|
configuration: config
|
||||||
)
|
)
|
||||||
isValid = true
|
isValid = true
|
||||||
|
Storage.internalHasCreatedValidInstance.mutate { $0 = true }
|
||||||
}
|
}
|
||||||
catch {}
|
catch { startupError = error }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Migrations
|
// MARK: - Migrations
|
||||||
|
@ -118,7 +123,12 @@ open class Storage {
|
||||||
onProgressUpdate: ((CGFloat, TimeInterval) -> ())?,
|
onProgressUpdate: ((CGFloat, TimeInterval) -> ())?,
|
||||||
onComplete: @escaping (Swift.Result<Void, Error>, Bool) -> ()
|
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)
|
typealias MigrationInfo = (identifier: TargetMigrations.Identifier, migrations: TargetMigrations.MigrationSet)
|
||||||
let sortedMigrationInfo: [MigrationInfo] = migrations
|
let sortedMigrationInfo: [MigrationInfo] = migrations
|
||||||
|
@ -135,11 +145,11 @@ open class Storage {
|
||||||
.reduce(into: []) { result, next in result.append(contentsOf: next) }
|
.reduce(into: []) { result, next in result.append(contentsOf: next) }
|
||||||
|
|
||||||
// Setup and run any required migrations
|
// Setup and run any required migrations
|
||||||
migrator = {
|
migrator = { [weak self] in
|
||||||
var migrator: DatabaseMigrator = DatabaseMigrator()
|
var migrator: DatabaseMigrator = DatabaseMigrator()
|
||||||
sortedMigrationInfo.forEach { migrationInfo in
|
sortedMigrationInfo.forEach { migrationInfo in
|
||||||
migrationInfo.migrations.forEach { migration 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()
|
try? SUKLegacy.deleteLegacyDatabaseFilesAndKey()
|
||||||
|
|
||||||
Storage.shared.isValid = false
|
Storage.shared.isValid = false
|
||||||
|
Storage.internalHasCreatedValidInstance.mutate { $0 = false }
|
||||||
Storage.shared.migrationsCompleted.mutate { $0 = false }
|
Storage.shared.migrationsCompleted.mutate { $0 = false }
|
||||||
Storage.shared.dbWriter = nil
|
Storage.shared.dbWriter = nil
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Foundation
|
||||||
public enum StorageError: Error {
|
public enum StorageError: Error {
|
||||||
case generic
|
case generic
|
||||||
case databaseInvalid
|
case databaseInvalid
|
||||||
|
case startupFailed
|
||||||
case migrationFailed
|
case migrationFailed
|
||||||
case invalidKeySpec
|
case invalidKeySpec
|
||||||
case decodingFailed
|
case decodingFailed
|
||||||
|
|
|
@ -13,15 +13,16 @@ public protocol Migration {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension 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
|
return { (db: Database) in
|
||||||
SNLogNotTests("[Migration Info] Starting \(targetIdentifier.key(with: self))")
|
SNLogNotTests("[Migration Info] Starting \(targetIdentifier.key(with: self))")
|
||||||
Storage.shared.internalCurrentlyRunningMigration.mutate { $0 = (targetIdentifier, self) }
|
storage?.internalCurrentlyRunningMigration.mutate { $0 = (targetIdentifier, self) }
|
||||||
do { try migrate(db) }
|
defer { storage?.internalCurrentlyRunningMigration.mutate { $0 = nil } }
|
||||||
catch {
|
|
||||||
Storage.shared.internalCurrentlyRunningMigration.mutate { $0 = nil }
|
try migrate(db)
|
||||||
throw error
|
|
||||||
}
|
|
||||||
SNLogNotTests("[Migration Info] Completed \(targetIdentifier.key(with: self))")
|
SNLogNotTests("[Migration Info] Completed \(targetIdentifier.key(with: self))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,15 @@ import Foundation
|
||||||
import GRDB
|
import GRDB
|
||||||
|
|
||||||
public extension DatabaseMigrator {
|
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(
|
self.registerMigration(
|
||||||
targetIdentifier.key(with: migration),
|
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 {
|
beforeEach {
|
||||||
var migrator: DatabaseMigrator = DatabaseMigrator()
|
var migrator: DatabaseMigrator = DatabaseMigrator()
|
||||||
migrator.registerMigration(
|
migrator.registerMigration(
|
||||||
TestAddColumnMigration.target,
|
mockStorage,
|
||||||
|
targetIdentifier: TestAddColumnMigration.target,
|
||||||
migration: TestAddColumnMigration.self
|
migration: TestAddColumnMigration.self
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ open class ScreenLockViewController: UIViewController {
|
||||||
open override func loadView() {
|
open override func loadView() {
|
||||||
super.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()
|
let edgesView: UIView = UIView.container()
|
||||||
self.view.addSubview(edgesView)
|
self.view.addSubview(edgesView)
|
||||||
|
|
Loading…
Reference in a new issue