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:
Morgan Pretty 2023-06-19 18:19:47 +10:00
parent d2c82cb915
commit f07313c7ac
52 changed files with 596 additions and 286 deletions

@ -1 +1 @@
Subproject commit 9777b37e8545febcc082578341352dba7433db21 Subproject commit 49c78682a6f4546c8773113f3e201244f0b1e65a

View file

@ -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)",

View file

@ -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 {

View file

@ -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 {

View file

@ -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,

View file

@ -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:

View 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?) {}
}

View file

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

View file

@ -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()
}

View file

@ -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(

View file

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

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 %@.";

View file

@ -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 {

View file

@ -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(

View file

@ -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
} }

View file

@ -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(

View file

@ -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 }

View file

@ -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()
) )
} }
} }

View file

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

View file

@ -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
] ]
} }

View file

@ -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
] ]
} }

View file

@ -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
] ]
} }

View file

@ -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
] ]
} }

View file

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

View file

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

View file

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

View file

@ -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))")
} }
} }

View file

@ -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)
) )
} }
} }

View file

@ -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() }
}
}

View file

@ -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
) )

View file

@ -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)