Merge remote-tracking branch 'upstream/dev' into feature/updated-user-config-handling

# Conflicts:
#	Session.xcodeproj/project.pbxproj
#	Session/Calls/CallVC.swift
#	Session/Conversations/Message Cells/Content Views/DocumentView.swift
#	Session/Conversations/Settings/OWSMessageTimerView.m
#	SessionMessagingKit/File Server/FileServerAPI.swift
#	SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift
#	SessionMessagingKit/Open Groups/OpenGroupManager.swift
#	SessionMessagingKit/Sending & Receiving/MessageReceiver.swift
#	SessionMessagingKit/Shared Models/MessageViewModel.swift
#	SessionMessagingKit/Shared Models/SessionThreadViewModel.swift
#	SessionSnodeKit/SnodeAPI.swift
#	SessionUtilitiesKit/Networking/HTTP.swift
#	SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift
This commit is contained in:
Morgan Pretty 2023-04-13 13:22:39 +10:00
commit fa39b5f61c
76 changed files with 1596 additions and 435 deletions

View File

@ -109,6 +109,13 @@
7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52DC28580D50006069F2 /* EmojiSkinTonePicker.swift */; };
7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */; };
7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */; };
7B2561C22978B307005C086C /* MediaInfoVC+MediaInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2561C12978B307005C086C /* MediaInfoVC+MediaInfoView.swift */; };
7B2561C429874851005C086C /* SessionCarouselView+Info.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2561C329874851005C086C /* SessionCarouselView+Info.swift */; };
7B2E985829AC227C001792D7 /* UIContextualAction+Theming.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */; };
7B3A392E2977791E002FE4AC /* MediaInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A392D2977791E002FE4AC /* MediaInfoVC.swift */; };
7B3A3930297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A392F297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift */; };
7B3A39322980D02B002FE4AC /* SessionCarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A39312980D02B002FE4AC /* SessionCarouselView.swift */; };
7B3A3934298882D6002FE4AC /* SessionCarouselViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B3A3933298882D6002FE4AC /* SessionCarouselViewDelegate.swift */; };
7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */; };
7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; };
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; };
@ -178,9 +185,6 @@
B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80A579E23DFF1F300876683 /* NewClosedGroupVC.swift */; };
B817AD9A26436593009DF825 /* SimplifiedConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */; };
B817AD9C26436F73009DF825 /* ThreadPickerVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B817AD9B26436F73009DF825 /* ThreadPickerVC.swift */; };
B81D25C426157F40004D1FE1 /* storage-seed-3.crt in Resources */ = {isa = PBXBuildFile; fileRef = B81D25B926157F20004D1FE1 /* storage-seed-3.crt */; };
B81D25C526157F40004D1FE1 /* storage-seed-1.crt in Resources */ = {isa = PBXBuildFile; fileRef = B81D25B726157F20004D1FE1 /* storage-seed-1.crt */; };
B81D25C626157F40004D1FE1 /* public-loki-foundation.crt in Resources */ = {isa = PBXBuildFile; fileRef = B81D25B826157F20004D1FE1 /* public-loki-foundation.crt */; };
B82149C125D605C6009C0F2A /* InfoBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82149C025D605C6009C0F2A /* InfoBanner.swift */; };
B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D2825C7A4B400488AB4 /* InputView.swift */; };
B8269D3325C7A8C600488AB4 /* InputViewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D3225C7A8C600488AB4 /* InputViewButton.swift */; };
@ -418,9 +422,6 @@
C38EF407255B6DF7007E1867 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E9255B6DF6007E1867 /* Toast.swift */; };
C38EF40B255B6DF7007E1867 /* TappableStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3ED255B6DF6007E1867 /* TappableStackView.swift */; };
C38EF48A255B7E3F007E1867 /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; };
C3A01E05261D24C400290BEB /* public-loki-foundation.der in Resources */ = {isa = PBXBuildFile; fileRef = C3A01E02261D24C400290BEB /* public-loki-foundation.der */; };
C3A01E06261D24C400290BEB /* storage-seed-1.der in Resources */ = {isa = PBXBuildFile; fileRef = C3A01E03261D24C400290BEB /* storage-seed-1.der */; };
C3A01E07261D24C400290BEB /* storage-seed-3.der in Resources */ = {isa = PBXBuildFile; fileRef = C3A01E04261D24C400290BEB /* storage-seed-3.der */; };
C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A3A170256E1D25004D228D /* SSKReachabilityManager.swift */; };
C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D0A2558989C0043A11F /* MessageWrapper.swift */; };
C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D1C25589AC30043A11F /* WebSocketProto.swift */; };
@ -818,6 +819,12 @@
FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */; };
FDE658A129418C7900A33BC1 /* CryptoKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE658A029418C7900A33BC1 /* CryptoKit+Utilities.swift */; };
FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE658A229418E2F00A33BC1 /* KeyPair.swift */; };
FDDCBDA829E776BF00303C38 /* seed2-2023-2y.crt in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA229E776BF00303C38 /* seed2-2023-2y.crt */; };
FDDCBDA929E776BF00303C38 /* seed1-2023-2y.crt in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA329E776BF00303C38 /* seed1-2023-2y.crt */; };
FDDCBDAA29E776BF00303C38 /* seed1-2023-2y.der in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA429E776BF00303C38 /* seed1-2023-2y.der */; };
FDDCBDAB29E776BF00303C38 /* seed2-2023-2y.der in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA529E776BF00303C38 /* seed2-2023-2y.der */; };
FDDCBDAC29E776BF00303C38 /* seed3-2023-2y.crt in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA629E776BF00303C38 /* seed3-2023-2y.crt */; };
FDDCBDAD29E776BF00303C38 /* seed3-2023-2y.der in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA729E776BF00303C38 /* seed3-2023-2y.der */; };
FDE77F6B280FEB28002CFC5D /* ControlMessageProcessRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */; };
FDED2E3C282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDED2E3B282E1B5D00B2CD2A /* UICollectionView+ReusableView.swift */; };
FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */; };
@ -1224,8 +1231,14 @@
7B1B52DC28580D50006069F2 /* EmojiSkinTonePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiSkinTonePicker.swift; sourceTree = "<group>"; };
7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSENotificationPresenter.swift; sourceTree = "<group>"; };
7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = "<group>"; };
7B2561C12978B307005C086C /* MediaInfoVC+MediaInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaInfoVC+MediaInfoView.swift"; sourceTree = "<group>"; };
7B2561C329874851005C086C /* SessionCarouselView+Info.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCarouselView+Info.swift"; sourceTree = "<group>"; };
7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = "<group>"; };
7B2E985729AC227C001792D7 /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = "<group>"; };
7B3A392D2977791E002FE4AC /* MediaInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaInfoVC.swift; sourceTree = "<group>"; };
7B3A392F297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaInfoVC+MediaPreviewView.swift"; sourceTree = "<group>"; };
7B3A39312980D02B002FE4AC /* SessionCarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCarouselView.swift; sourceTree = "<group>"; };
7B3A3933298882D6002FE4AC /* SessionCarouselViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionCarouselViewDelegate.swift; sourceTree = "<group>"; };
7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.swift; sourceTree = "<group>"; };
7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = "<group>"; };
7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = "<group>"; };
@ -1315,9 +1328,6 @@
B80A579E23DFF1F300876683 /* NewClosedGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewClosedGroupVC.swift; sourceTree = "<group>"; };
B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimplifiedConversationCell.swift; sourceTree = "<group>"; };
B817AD9B26436F73009DF825 /* ThreadPickerVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerVC.swift; sourceTree = "<group>"; };
B81D25B726157F20004D1FE1 /* storage-seed-1.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "storage-seed-1.crt"; sourceTree = "<group>"; };
B81D25B826157F20004D1FE1 /* public-loki-foundation.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "public-loki-foundation.crt"; sourceTree = "<group>"; };
B81D25B926157F20004D1FE1 /* storage-seed-3.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "storage-seed-3.crt"; sourceTree = "<group>"; };
B82149C025D605C6009C0F2A /* InfoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoBanner.swift; sourceTree = "<group>"; };
B8269D2825C7A4B400488AB4 /* InputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputView.swift; sourceTree = "<group>"; };
B8269D3225C7A8C600488AB4 /* InputViewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputViewButton.swift; sourceTree = "<group>"; };
@ -1585,9 +1595,6 @@
C396469D2509D3F400B0B9F5 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
C396469E2509D40400B0B9F5 /* vi-VN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "vi-VN"; path = "vi-VN.lproj/Localizable.strings"; sourceTree = "<group>"; };
C396469F2509D41100B0B9F5 /* id-ID */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "id-ID"; path = "id-ID.lproj/Localizable.strings"; sourceTree = "<group>"; };
C3A01E02261D24C400290BEB /* public-loki-foundation.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "public-loki-foundation.der"; sourceTree = "<group>"; };
C3A01E03261D24C400290BEB /* storage-seed-1.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "storage-seed-1.der"; sourceTree = "<group>"; };
C3A01E04261D24C400290BEB /* storage-seed-3.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "storage-seed-3.der"; sourceTree = "<group>"; };
C3A3A170256E1D25004D228D /* SSKReachabilityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKReachabilityManager.swift; sourceTree = "<group>"; };
C3A71D0A2558989C0043A11F /* MessageWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageWrapper.swift; sourceTree = "<group>"; };
C3A71D1C25589AC30043A11F /* WebSocketProto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebSocketProto.swift; sourceTree = "<group>"; };
@ -1951,6 +1958,12 @@
FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchRequest+Utilities.swift"; sourceTree = "<group>"; };
FDE658A029418C7900A33BC1 /* CryptoKit+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CryptoKit+Utilities.swift"; sourceTree = "<group>"; };
FDE658A229418E2F00A33BC1 /* KeyPair.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyPair.swift; sourceTree = "<group>"; };
FDDCBDA229E776BF00303C38 /* seed2-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed2-2023-2y.crt"; sourceTree = "<group>"; };
FDDCBDA329E776BF00303C38 /* seed1-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed1-2023-2y.crt"; sourceTree = "<group>"; };
FDDCBDA429E776BF00303C38 /* seed1-2023-2y.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "seed1-2023-2y.der"; sourceTree = "<group>"; };
FDDCBDA529E776BF00303C38 /* seed2-2023-2y.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "seed2-2023-2y.der"; sourceTree = "<group>"; };
FDDCBDA629E776BF00303C38 /* seed3-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed3-2023-2y.crt"; sourceTree = "<group>"; };
FDDCBDA729E776BF00303C38 /* seed3-2023-2y.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "seed3-2023-2y.der"; sourceTree = "<group>"; };
FDE7214F287E50D50093DF33 /* ProtoWrappers.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ProtoWrappers.py; sourceTree = "<group>"; };
FDE72150287E50D50093DF33 /* LintLocalizableStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LintLocalizableStrings.swift; sourceTree = "<group>"; };
FDE77F68280F9EDA002CFC5D /* JobRunnerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerError.swift; sourceTree = "<group>"; };
@ -2462,12 +2475,12 @@
B81D260326158DF5004D1FE1 /* Certificates */ = {
isa = PBXGroup;
children = (
B81D25B826157F20004D1FE1 /* public-loki-foundation.crt */,
C3A01E02261D24C400290BEB /* public-loki-foundation.der */,
B81D25B726157F20004D1FE1 /* storage-seed-1.crt */,
C3A01E03261D24C400290BEB /* storage-seed-1.der */,
B81D25B926157F20004D1FE1 /* storage-seed-3.crt */,
C3A01E04261D24C400290BEB /* storage-seed-3.der */,
FDDCBDA329E776BF00303C38 /* seed1-2023-2y.crt */,
FDDCBDA429E776BF00303C38 /* seed1-2023-2y.der */,
FDDCBDA229E776BF00303C38 /* seed2-2023-2y.crt */,
FDDCBDA529E776BF00303C38 /* seed2-2023-2y.der */,
FDDCBDA629E776BF00303C38 /* seed3-2023-2y.crt */,
FDDCBDA729E776BF00303C38 /* seed3-2023-2y.der */,
);
path = Certificates;
sourceTree = "<group>";
@ -2678,6 +2691,9 @@
FD52090828B59411006098F6 /* ScreenLockUI.swift */,
FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */,
FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */,
7B3A39312980D02B002FE4AC /* SessionCarouselView.swift */,
7B2561C329874851005C086C /* SessionCarouselView+Info.swift */,
7B3A3933298882D6002FE4AC /* SessionCarouselViewDelegate.swift */,
);
path = Shared;
sourceTree = "<group>";
@ -3083,6 +3099,9 @@
4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */,
4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */,
7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */,
7B3A392D2977791E002FE4AC /* MediaInfoVC.swift */,
7B3A392F297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift */,
7B2561C12978B307005C086C /* MediaInfoVC+MediaInfoView.swift */,
);
path = "Media Viewing & Editing";
sourceTree = "<group>";
@ -4926,19 +4945,16 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B81D25C526157F40004D1FE1 /* storage-seed-1.crt in Resources */,
B81D25C426157F40004D1FE1 /* storage-seed-3.crt in Resources */,
B81D25C626157F40004D1FE1 /* public-loki-foundation.crt in Resources */,
4C63CC00210A620B003AE45C /* SignalTSan.supp in Resources */,
4C6F527C20FFE8400097DEEE /* SignalUBSan.supp in Resources */,
34CF078A203E6B78005C4D61 /* end_call_tone_cept.caf in Resources */,
C3CA3AA2255CDADA00F4C6D4 /* english.txt in Resources */,
B6F509971AA53F760068F56A /* Localizable.strings in Resources */,
C3A01E05261D24C400290BEB /* public-loki-foundation.der in Resources */,
B66DBF4A19D5BBC8006EA940 /* Images.xcassets in Resources */,
34CF0788203E6B78005C4D61 /* ringback_tone_ansi.caf in Resources */,
7BFD1A972747689000FB91B9 /* Session-Turn-Server in Resources */,
34C3C78F2040A4F70000134C /* sonarping.mp3 in Resources */,
FDDCBDA929E776BF00303C38 /* seed1-2023-2y.crt in Resources */,
34661FB820C1C0D60056EDD6 /* message_sent.aiff in Resources */,
45CB2FA81CB7146C00E1B343 /* Launch Screen.storyboard in Resources */,
34C3C78D20409F320000134C /* Opening.m4r in Resources */,
@ -4950,12 +4966,12 @@
45B74A742044AAB600CD42F8 /* aurora-quiet.aifc in Resources */,
7B0EFDF4275490EA00FFAAE7 /* ringing.mp3 in Resources */,
45B74A852044AAB600CD42F8 /* bamboo.aifc in Resources */,
C3A01E06261D24C400290BEB /* storage-seed-1.der in Resources */,
45B74A782044AAB600CD42F8 /* bamboo-quiet.aifc in Resources */,
45B74A7B2044AAB600CD42F8 /* chord.aifc in Resources */,
45B74A812044AAB600CD42F8 /* chord-quiet.aifc in Resources */,
45B74A832044AAB600CD42F8 /* circles.aifc in Resources */,
45B74A892044AAB600CD42F8 /* circles-quiet.aifc in Resources */,
FDDCBDAA29E776BF00303C38 /* seed1-2023-2y.der in Resources */,
C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */,
4503F1BF20470A5B00CEE724 /* classic.aifc in Resources */,
B8D07405265C683300F77E07 /* ElegantIcons.ttf in Resources */,
@ -4968,8 +4984,11 @@
45B74A7C2044AAB600CD42F8 /* hello-quiet.aifc in Resources */,
7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */,
45B74A792044AAB600CD42F8 /* input.aifc in Resources */,
FDDCBDAB29E776BF00303C38 /* seed2-2023-2y.der in Resources */,
C3CA3ABE255CDB0D00F4C6D4 /* portuguese.txt in Resources */,
45B74A8C2044AAB600CD42F8 /* input-quiet.aifc in Resources */,
FDDCBDAC29E776BF00303C38 /* seed3-2023-2y.crt in Resources */,
FDDCBDA829E776BF00303C38 /* seed2-2023-2y.crt in Resources */,
45B74A7A2044AAB600CD42F8 /* keys.aifc in Resources */,
45B74A762044AAB600CD42F8 /* keys-quiet.aifc in Resources */,
45B74A862044AAB600CD42F8 /* note.aifc in Resources */,
@ -4979,7 +4998,7 @@
45B74A822044AAB600CD42F8 /* pulse.aifc in Resources */,
C3CA3AC8255CDB2900F4C6D4 /* spanish.txt in Resources */,
B8FF8E6225C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 in Resources */,
C3A01E07261D24C400290BEB /* storage-seed-3.der in Resources */,
FDDCBDAD29E776BF00303C38 /* seed3-2023-2y.der in Resources */,
45B74A802044AAB600CD42F8 /* pulse-quiet.aifc in Resources */,
45B74A8B2044AAB600CD42F8 /* synth.aifc in Resources */,
45B74A752044AAB600CD42F8 /* synth-quiet.aifc in Resources */,
@ -5869,6 +5888,7 @@
buildActionMask = 2147483647;
files = (
FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */,
7B2561C429874851005C086C /* SessionCarouselView+Info.swift in Sources */,
FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */,
FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */,
B8041AA725C90927003C2166 /* TypingIndicatorCell.swift in Sources */,
@ -5883,6 +5903,7 @@
7BAF54D027ACCEEC003D12F8 /* EmptySearchResultCell.swift in Sources */,
B8783E9E23EB948D00404FB8 /* UILabel+Interaction.swift in Sources */,
FD37E9D928A230F2003AE748 /* TraitObservingWindow.swift in Sources */,
7B3A3930297A3919002FE4AC /* MediaInfoVC+MediaPreviewView.swift in Sources */,
B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */,
FD848B8F283EF2A8000E298B /* UIScrollView+Utilities.swift in Sources */,
B879D449247E1BE300DB3608 /* PathVC.swift in Sources */,
@ -5925,6 +5946,8 @@
C3548F0624456447009433A8 /* PNModeVC.swift in Sources */,
FD71164828E2CE8700B47552 /* SessionCell+AccessoryView.swift in Sources */,
B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */,
FD71163A28E2C53700B47552 /* SessionAvatarCell.swift in Sources */,
7B3A392E2977791E002FE4AC /* MediaInfoVC.swift in Sources */,
7BA68909272A27BE00EFC32F /* SessionCall.swift in Sources */,
B835247925C38D880089A44F /* MessageCell.swift in Sources */,
B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */,
@ -5985,6 +6008,7 @@
FD71164228E2C85A00B47552 /* TransitionType.swift in Sources */,
FD848B9828422F1A000E298B /* Date+Utilities.swift in Sources */,
FD37E9DB28A244E9003AE748 /* ThemePreviewView.swift in Sources */,
7B3A3934298882D6002FE4AC /* SessionCarouselViewDelegate.swift in Sources */,
B85357C323A1BD1200AAF6CD /* SeedVC.swift in Sources */,
45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */,
7B9F71C928470667006DFE7B /* ReactionListSheet.swift in Sources */,
@ -6032,6 +6056,7 @@
FD39352C28F382920084DADA /* VersionFooterView.swift in Sources */,
7B9F71D22852EEE2006DFE7B /* Emoji+SkinTones.swift in Sources */,
7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */,
7B2561C22978B307005C086C /* MediaInfoVC+MediaInfoView.swift in Sources */,
B8569AE325CBB19A00DBA3DB /* DocumentView.swift in Sources */,
FDF848F529413EEC007DCAE5 /* SessionCell+Styling.swift in Sources */,
7BFD1A8A2745C4F000FB91B9 /* Permissions.swift in Sources */,
@ -6055,6 +6080,7 @@
FD71163828E2C50700B47552 /* SessionTableViewModel.swift in Sources */,
FD71164A28E3EA5B00B47552 /* DismissType.swift in Sources */,
C328251F25CA3A900062D0A7 /* QuoteView.swift in Sources */,
7B3A39322980D02B002FE4AC /* SessionCarouselView.swift in Sources */,
FD37E9CC28A1E578003AE748 /* AppearanceViewController.swift in Sources */,
B8EB20F02640F7F000773E52 /* OpenGroupInvitationView.swift in Sources */,
C328254025CA55880062D0A7 /* ContextMenuVC.swift in Sources */,
@ -6341,7 +6367,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 399;
CURRENT_PROJECT_VERSION = 401;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6365,7 +6391,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.10;
MARKETING_VERSION = 2.2.12;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -6413,7 +6439,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 399;
CURRENT_PROJECT_VERSION = 401;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6442,7 +6468,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.10;
MARKETING_VERSION = 2.2.12;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -6478,7 +6504,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 399;
CURRENT_PROJECT_VERSION = 401;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6501,7 +6527,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.10;
MARKETING_VERSION = 2.2.12;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -6552,7 +6578,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 399;
CURRENT_PROJECT_VERSION = 401;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6580,7 +6606,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.2.10;
MARKETING_VERSION = 2.2.12;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -7464,7 +7490,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 399;
CURRENT_PROJECT_VERSION = 401;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7502,7 +7528,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.2.10;
MARKETING_VERSION = 2.2.12;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -7535,7 +7561,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 399;
CURRENT_PROJECT_VERSION = 401;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -7573,7 +7599,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.2.10;
MARKETING_VERSION = 2.2.12;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session;

View File

@ -372,6 +372,10 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
webRTCSession.attachLocalRenderer(renderer)
}
func removeLocalVideoRenderer(_ renderer: RTCVideoRenderer) {
webRTCSession.removeLocalRenderer(renderer)
}
// MARK: - Delegate
public func webRTCIsConnected() {

View File

@ -10,6 +10,8 @@ import SessionUtilitiesKit
final class CallVC: UIViewController, VideoPreviewDelegate {
private static let avatarRadius: CGFloat = (isIPhone6OrSmaller ? 100 : 120)
private static let floatingVideoViewWidth: CGFloat = (UIDevice.current.isIPad ? 160 : 80)
private static let floatingVideoViewHeight: CGFloat = (UIDevice.current.isIPad ? 346: 173)
let call: SessionCall
var latestKnownAudioOutputDeviceName: String?
@ -24,27 +26,90 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
return result
}()
enum FloatingViewVideoSource {
case local
case remote
}
var floatingViewVideoSource: FloatingViewVideoSource = .local
// MARK: - UI Components
private lazy var localVideoView: LocalVideoView = {
private lazy var floatingLocalVideoView: LocalVideoView = {
let result = LocalVideoView()
result.clipsToBounds = true
result.alpha = 0
result.themeBackgroundColor = .backgroundSecondary
result.isHidden = !call.isVideoEnabled
result.layer.cornerRadius = UIDevice.current.isIPad ? 20 : 10
result.layer.masksToBounds = true
result.set(.width, to: LocalVideoView.width)
result.set(.height, to: LocalVideoView.height)
result.makeViewDraggable()
result.set(.width, to: Self.floatingVideoViewWidth)
result.set(.height, to: Self.floatingVideoViewHeight)
return result
}()
private lazy var remoteVideoView: RemoteVideoView = {
private lazy var floatingRemoteVideoView: RemoteVideoView = {
let result = RemoteVideoView()
result.alpha = 0
result.themeBackgroundColor = .backgroundSecondary
result.set(.width, to: Self.floatingVideoViewWidth)
result.set(.height, to: Self.floatingVideoViewHeight)
return result
}()
private lazy var fullScreenLocalVideoView: LocalVideoView = {
let result = LocalVideoView()
result.alpha = 0
result.themeBackgroundColor = .backgroundPrimary
result.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleFullScreenVideoViewTapped)))
return result
}()
private lazy var fullScreenRemoteVideoView: RemoteVideoView = {
let result = RemoteVideoView()
result.alpha = 0
result.themeBackgroundColor = .backgroundPrimary
result.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleRemoteVieioViewTapped)))
result.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleFullScreenVideoViewTapped)))
return result
}()
private lazy var floatingViewContainer: UIView = {
let result = UIView()
result.isHidden = true
result.clipsToBounds = true
result.layer.cornerRadius = UIDevice.current.isIPad ? 20 : 10
result.layer.masksToBounds = true
result.themeBackgroundColor = .backgroundSecondary
result.makeViewDraggable()
let noVideoIcon: UIImageView = UIImageView(
image: UIImage(systemName: "video.slash")?
.withRenderingMode(.alwaysTemplate)
)
noVideoIcon.themeTintColor = .textPrimary
noVideoIcon.set(.width, to: 34)
noVideoIcon.set(.height, to: 28)
result.addSubview(noVideoIcon)
noVideoIcon.center(in: result)
result.addSubview(floatingLocalVideoView)
floatingLocalVideoView.pin(to: result)
result.addSubview(floatingRemoteVideoView)
floatingRemoteVideoView.pin(to: result)
let swappingVideoIcon: UIImageView = UIImageView(
image: UIImage(systemName: "arrow.2.squarepath")?
.withRenderingMode(.alwaysTemplate)
)
swappingVideoIcon.themeTintColor = .textPrimary
swappingVideoIcon.set(.width, to: 16)
swappingVideoIcon.set(.height, to: 12)
result.addSubview(swappingVideoIcon)
swappingVideoIcon.pin(.top, to: .top, of: result, withInset: Values.smallSpacing)
swappingVideoIcon.pin(.trailing, to: .trailing, of: result, withInset: -Values.smallSpacing)
result.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(switchVideo)))
return result
}()
@ -280,7 +345,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
self.call.remoteVideoStateDidChange = { isEnabled in
DispatchQueue.main.async {
UIView.animate(withDuration: 0.25) {
self.remoteVideoView.alpha = isEnabled ? 1 : 0
let remoteVideoView: RemoteVideoView = self.floatingViewVideoSource == .remote ? self.floatingRemoteVideoView : self.fullScreenRemoteVideoView
remoteVideoView.alpha = isEnabled ? 1 : 0
}
if self.callInfoLabel.alpha < 0.5 {
@ -361,7 +427,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
if shouldRestartCamera { cameraManager.prepare() }
touch(call.videoCapturer)
_ = call.videoCapturer // Force the lazy var to instantiate
titleLabel.text = self.call.contactName
AppEnvironment.shared.callManager.startCall(call) { [weak self] error in
DispatchQueue.main.async {
@ -390,13 +456,16 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
view.addSubview(profilePictureContainer)
// Remote video view
call.attachRemoteVideoRenderer(remoteVideoView)
view.addSubview(remoteVideoView)
remoteVideoView.translatesAutoresizingMaskIntoConstraints = false
remoteVideoView.pin(to: view)
call.attachRemoteVideoRenderer(fullScreenRemoteVideoView)
view.addSubview(fullScreenRemoteVideoView)
fullScreenRemoteVideoView.translatesAutoresizingMaskIntoConstraints = false
fullScreenRemoteVideoView.pin(to: view)
// Local video view
call.attachLocalVideoRenderer(localVideoView)
call.attachLocalVideoRenderer(floatingLocalVideoView)
view.addSubview(fullScreenLocalVideoView)
fullScreenLocalVideoView.translatesAutoresizingMaskIntoConstraints = false
fullScreenLocalVideoView.pin(to: view)
// Fade view
view.addSubview(fadeView)
@ -413,8 +482,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
view.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.center(.vertical, in: minimizeButton)
titleLabel.pin(.left, to: .left, of: view, withInset: Values.largeSpacing)
titleLabel.pin(.right, to: .right, of: view, withInset: Values.largeSpacing)
titleLabel.pin(.leading, to: .leading, of: view, withInset: Values.largeSpacing)
titleLabel.pin(.trailing, to: .trailing, of: view, withInset: -Values.largeSpacing)
// Response Panel
view.addSubview(responsePanel)
@ -449,12 +518,12 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
callDurationLabel.center(in: callInfoLabelContainer)
}
private func addLocalVideoView() {
private func addFloatingVideoView() {
let safeAreaInsets = UIApplication.shared.keyWindow?.safeAreaInsets
CurrentAppContext().mainWindow?.addSubview(localVideoView)
localVideoView.autoPinEdge(toSuperviewEdge: .right, withInset: Values.smallSpacing)
CurrentAppContext().mainWindow?.addSubview(floatingViewContainer)
floatingViewContainer.autoPinEdge(toSuperviewEdge: .right, withInset: Values.smallSpacing)
let topMargin = (safeAreaInsets?.top ?? 0) + Values.veryLargeSpacing
localVideoView.autoPinEdge(toSuperviewEdge: .top, withInset: topMargin)
floatingViewContainer.autoPinEdge(toSuperviewEdge: .top, withInset: topMargin)
}
override func viewDidAppear(_ animated: Bool) {
@ -463,7 +532,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.start() }
shouldRestartCamera = true
addLocalVideoView()
addFloatingVideoView()
let remoteVideoView: RemoteVideoView = self.floatingViewVideoSource == .remote ? self.floatingRemoteVideoView : self.fullScreenRemoteVideoView
remoteVideoView.alpha = (call.isRemoteVideoEnabled ? 1 : 0)
}
@ -472,7 +542,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.stop() }
localVideoView.removeFromSuperview()
floatingViewContainer.removeFromSuperview()
}
// MARK: - Orientation
@ -519,7 +589,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
self.callInfoLabel.text = "Call Ended"
UIView.animate(withDuration: 0.25) {
self.remoteVideoView.alpha = 0
let remoteVideoView: RemoteVideoView = self.floatingViewVideoSource == .remote ? self.floatingRemoteVideoView : self.fullScreenRemoteVideoView
remoteVideoView.alpha = 0
self.operationPanel.alpha = 1
self.responsePanel.alpha = 1
self.callInfoLabel.alpha = 1
@ -577,7 +648,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
@objc private func operateCamera() {
if (call.isVideoEnabled) {
localVideoView.isHidden = true
floatingViewContainer.isHidden = true
cameraManager.stop()
videoButton.themeTintColor = .textPrimary
videoButton.themeBackgroundColor = .backgroundSecondary
@ -593,7 +664,9 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
}
func cameraDidConfirmTurningOn() {
localVideoView.isHidden = false
floatingViewContainer.isHidden = false
let localVideoView: LocalVideoView = self.floatingViewVideoSource == .local ? self.floatingLocalVideoView : self.fullScreenLocalVideoView
localVideoView.alpha = 1
cameraManager.prepare()
cameraManager.start()
videoButton.themeTintColor = .backgroundSecondary
@ -602,6 +675,34 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
call.isVideoEnabled = true
}
@objc private func switchVideo() {
if self.floatingViewVideoSource == .remote {
call.removeRemoteVideoRenderer(self.floatingRemoteVideoView)
call.removeLocalVideoRenderer(self.fullScreenLocalVideoView)
self.floatingRemoteVideoView.alpha = 0
self.floatingLocalVideoView.alpha = call.isVideoEnabled ? 1 : 0
self.fullScreenRemoteVideoView.alpha = call.isRemoteVideoEnabled ? 1 : 0
self.fullScreenLocalVideoView.alpha = 0
self.floatingViewVideoSource = .local
call.attachRemoteVideoRenderer(self.fullScreenRemoteVideoView)
call.attachLocalVideoRenderer(self.floatingLocalVideoView)
} else {
call.removeRemoteVideoRenderer(self.fullScreenRemoteVideoView)
call.removeLocalVideoRenderer(self.floatingLocalVideoView)
self.floatingRemoteVideoView.alpha = call.isRemoteVideoEnabled ? 1 : 0
self.floatingLocalVideoView.alpha = 0
self.fullScreenRemoteVideoView.alpha = 0
self.fullScreenLocalVideoView.alpha = call.isVideoEnabled ? 1 : 0
self.floatingViewVideoSource = .remote
call.attachRemoteVideoRenderer(self.floatingRemoteVideoView)
call.attachLocalVideoRenderer(self.fullScreenLocalVideoView)
}
}
@objc private func switchCamera() {
cameraManager.switchCamera()
}
@ -663,7 +764,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
}
}
@objc private func handleRemoteVieioViewTapped(gesture: UITapGestureRecognizer) {
@objc private func handleFullScreenVideoViewTapped(gesture: UITapGestureRecognizer) {
let isHidden = callDurationLabel.alpha < 0.5
UIView.animate(withDuration: 0.5) {

View File

@ -89,10 +89,7 @@ class RemoteVideoView: TargetView {
// MARK: LocalVideoView
class LocalVideoView: TargetView {
static let width: CGFloat = UIDevice.current.isIPad ? 160 : 80
static let height: CGFloat = UIDevice.current.isIPad ? 346: 173
override func renderFrame(_ frame: RTCVideoFrame?) {
super.renderFrame(frame)

View File

@ -35,6 +35,14 @@ extension ContextMenuVC {
// MARK: - Actions
static func info(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action(
icon: UIImage(named: "ic_info"),
title: "context_menu_info".localized(),
accessibilityLabel: "Message info"
) { delegate?.info(cellViewModel) }
}
static func retry(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action(
icon: UIImage(systemName: "arrow.triangle.2.circlepath"),
@ -207,6 +215,8 @@ extension ContextMenuVC {
return !currentThreadIsMessageRequest
}()
let shouldShowInfo: Bool = (cellViewModel.attachments?.isEmpty == false)
let generatedActions: [Action] = [
(canRetry ? Action.retry(cellViewModel, delegate) : nil),
(canReply ? Action.reply(cellViewModel, delegate) : nil),
@ -216,6 +226,7 @@ extension ContextMenuVC {
(canDelete ? Action.delete(cellViewModel, delegate) : nil),
(canBan ? Action.ban(cellViewModel, delegate) : nil),
(canBan ? Action.banAndDeleteAllMessages(cellViewModel, delegate) : nil),
(shouldShowInfo ? Action.info(cellViewModel, delegate) : nil),
]
.appending(contentsOf: (shouldShowEmojiActions ? recentEmojis : []).map { Action.react(cellViewModel, $0, delegate) })
.appending(Action.emojiPlusButton(cellViewModel, delegate))
@ -230,6 +241,7 @@ extension ContextMenuVC {
// MARK: - Delegate
protocol ContextMenuActionDelegate {
func info(_ cellViewModel: MessageViewModel)
func retry(_ cellViewModel: MessageViewModel)
func reply(_ cellViewModel: MessageViewModel)
func copy(_ cellViewModel: MessageViewModel)

View File

@ -166,7 +166,9 @@ final class ContextMenuVC: UIViewController {
let menuStackView = UIStackView(
arrangedSubviews: actions
.filter { !$0.isEmojiAction && !$0.isEmojiPlus && !$0.isDismissAction }
.map { action -> ActionView in ActionView(for: action, dismiss: snDismiss) }
.map { action -> ActionView in
ActionView(for: action, dismiss: snDismiss)
}
)
menuStackView.axis = .vertical
menuBackgroundView.addSubview(menuStackView)

View File

@ -673,6 +673,10 @@ extension ConversationVC:
}
func inputTextViewDidChangeContent(_ inputTextView: InputTextView) {
// Note: If there is a 'draft' message then we don't want it to trigger the typing indicator to
// appear (as that is not expected/correct behaviour)
guard !viewIsAppearing else { return }
let newText: String = (inputTextView.text ?? "")
if !newText.isEmpty {
@ -1631,6 +1635,17 @@ extension ConversationVC:
// MARK: - ContextMenuActionDelegate
func info(_ cellViewModel: MessageViewModel) {
let mediaInfoVC = MediaInfoVC(
attachments: (cellViewModel.attachments ?? []),
isOutgoing: (cellViewModel.variant == .standardOutgoing),
threadId: self.viewModel.threadData.threadId,
threadVariant: self.viewModel.threadData.threadVariant,
interactionId: cellViewModel.id
)
navigationController?.pushViewController(mediaInfoVC, animated: true)
}
func retry(_ cellViewModel: MessageViewModel) {
Storage.shared.writeAsync { [weak self] db in
guard

View File

@ -501,11 +501,6 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Flag that the initial layout has been completed (the flag blocks and unblocks a number
// of different behaviours)
didFinishInitialLayout = true
viewIsAppearing = false
if delayFirstResponder || isShowingSearchUI {
delayFirstResponder = false
@ -517,7 +512,12 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
}
}
recoverInputView()
recoverInputView { [weak self] in
// Flag that the initial layout has been completed (the flag blocks and unblocks a number
// of different behaviours)
self?.didFinishInitialLayout = true
self?.viewIsAppearing = false
}
}
override func viewWillDisappear(_ animated: Bool) {
@ -1394,7 +1394,7 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
self.blockedBanner.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: self.view)
}
func recoverInputView() {
func recoverInputView(completion: (() -> ())? = nil) {
// This is a workaround for an issue where the textview is not scrollable
// after the app goes into background and goes back in foreground.
DispatchQueue.main.async {

View File

@ -30,7 +30,7 @@ public class MediaAlbumView: UIStackView {
mediaCache: mediaCache,
attachment: $0,
isOutgoing: isOutgoing,
maxMessageWidth: maxMessageWidth
cornerRadius: VisibleMessageCell.largeCornerRadius
)
}

View File

@ -17,10 +17,9 @@ public class MediaView: UIView {
// MARK: -
private let mediaCache: NSCache<NSString, AnyObject>
private let mediaCache: NSCache<NSString, AnyObject>?
public let attachment: Attachment
private let isOutgoing: Bool
private let maxMessageWidth: CGFloat
private var loadBlock: (() -> Void)?
private var unloadBlock: (() -> Void)?
@ -47,22 +46,21 @@ public class MediaView: UIView {
// MARK: - Initializers
public required init(
mediaCache: NSCache<NSString, AnyObject>,
mediaCache: NSCache<NSString, AnyObject>? = nil,
attachment: Attachment,
isOutgoing: Bool,
maxMessageWidth: CGFloat
cornerRadius: CGFloat
) {
self.mediaCache = mediaCache
self.attachment = attachment
self.isOutgoing = isOutgoing
self.maxMessageWidth = maxMessageWidth
super.init(frame: .zero)
themeBackgroundColor = .backgroundSecondary
clipsToBounds = true
layer.masksToBounds = true
layer.cornerRadius = VisibleMessageCell.largeCornerRadius
layer.cornerRadius = cornerRadius
createContents()
}
@ -397,7 +395,7 @@ public class MediaView: UIView {
applyMediaBlock(media)
self?.mediaCache.setObject(media, forKey: cacheKey as NSString)
self?.mediaCache?.setObject(media, forKey: cacheKey as NSString)
self?.loadState.mutate { $0 = .loaded }
}
@ -406,7 +404,7 @@ public class MediaView: UIView {
return
}
if let media: AnyObject = self.mediaCache.object(forKey: cacheKey as NSString) {
if let media: AnyObject = self.mediaCache?.object(forKey: cacheKey as NSString) {
Logger.verbose("media cache hit")
guard Thread.isMainThread else {

View File

@ -7,6 +7,7 @@
#import "UIView+OWS.h"
#import <QuartzCore/QuartzCore.h>
#import <SignalCoreKit/OWSAsserts.h>
#import <PureLayout/PureLayout.h>
#import <SignalCoreKit/NSDate+OWS.h>
#import <SessionUtilitiesKit/NSTimer+Proxying.h>
#import <SessionSnodeKit/SessionSnodeKit.h>

View File

@ -0,0 +1,191 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionUtilitiesKit
extension MediaInfoVC {
final class MediaInfoView: UIView {
private static let cornerRadius: CGFloat = 12
private var attachment: Attachment?
private let width: CGFloat = MediaInfoVC.mediaSize - 2 * MediaInfoVC.arrowSize.width
// MARK: - UI
private lazy var fileIdLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .systemFont(ofSize: Values.mediumFontSize)
result.themeTextColor = .textPrimary
return result
}()
private lazy var fileTypeLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .systemFont(ofSize: Values.mediumFontSize)
result.themeTextColor = .textPrimary
return result
}()
private lazy var fileSizeLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .systemFont(ofSize: Values.mediumFontSize)
result.themeTextColor = .textPrimary
return result
}()
private lazy var resolutionLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .systemFont(ofSize: Values.mediumFontSize)
result.themeTextColor = .textPrimary
return result
}()
private lazy var durationLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .systemFont(ofSize: Values.mediumFontSize)
result.themeTextColor = .textPrimary
return result
}()
// MARK: - Lifecycle
init(attachment: Attachment?) {
self.attachment = attachment
super.init(frame: CGRect.zero)
self.accessibilityLabel = "Media info"
setUpViewHierarchy()
update(attachment: attachment)
}
override init(frame: CGRect) {
preconditionFailure("Use init(attachment:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(attachment:) instead.")
}
private func setUpViewHierarchy() {
let backgroundView: UIView = UIView()
backgroundView.clipsToBounds = true
backgroundView.themeBackgroundColor = .contextMenu_background
backgroundView.layer.cornerRadius = Self.cornerRadius
addSubview(backgroundView)
backgroundView.pin(to: self)
let container: UIView = UIView()
container.set(.width, to: self.width)
// File ID
let fileIdTitleLabel: UILabel = {
let result = UILabel()
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.text = "ATTACHMENT_INFO_FILE_ID".localized() + ":"
result.themeTextColor = .textPrimary
return result
}()
let fileIdContainerStackView: UIStackView = UIStackView(arrangedSubviews: [ fileIdTitleLabel, fileIdLabel ])
fileIdContainerStackView.axis = .vertical
fileIdContainerStackView.spacing = 6
container.addSubview(fileIdContainerStackView)
fileIdContainerStackView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.top ], to: container)
// File Type
let fileTypeTitleLabel: UILabel = {
let result = UILabel()
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.text = "ATTACHMENT_INFO_FILE_TYPE".localized() + ":"
result.themeTextColor = .textPrimary
return result
}()
let fileTypeContainerStackView: UIStackView = UIStackView(arrangedSubviews: [ fileTypeTitleLabel, fileTypeLabel ])
fileTypeContainerStackView.axis = .vertical
fileTypeContainerStackView.spacing = 6
container.addSubview(fileTypeContainerStackView)
fileTypeContainerStackView.pin(.leading, to: .leading, of: container)
fileTypeContainerStackView.pin(.top, to: .bottom, of: fileIdContainerStackView, withInset: Values.largeSpacing)
// File Size
let fileSizeTitleLabel: UILabel = {
let result = UILabel()
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.text = "ATTACHMENT_INFO_FILE_SIZE".localized() + ":"
result.themeTextColor = .textPrimary
return result
}()
let fileSizeContainerStackView: UIStackView = UIStackView(arrangedSubviews: [ fileSizeTitleLabel, fileSizeLabel ])
fileSizeContainerStackView.axis = .vertical
fileSizeContainerStackView.spacing = 6
container.addSubview(fileSizeContainerStackView)
fileSizeContainerStackView.pin(.trailing, to: .trailing, of: container)
fileSizeContainerStackView.pin(.top, to: .bottom, of: fileIdContainerStackView, withInset: Values.largeSpacing)
fileSizeContainerStackView.set(.width, to: 90)
// Resolution
let resolutionTitleLabel: UILabel = {
let result = UILabel()
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.text = "ATTACHMENT_INFO_RESOLUTION".localized() + ":"
result.themeTextColor = .textPrimary
return result
}()
let resolutionContainerStackView: UIStackView = UIStackView(arrangedSubviews: [ resolutionTitleLabel, resolutionLabel ])
resolutionContainerStackView.axis = .vertical
resolutionContainerStackView.spacing = 6
container.addSubview(resolutionContainerStackView)
resolutionContainerStackView.pin(.leading, to: .leading, of: container)
resolutionContainerStackView.pin(.top, to: .bottom, of: fileTypeContainerStackView, withInset: Values.largeSpacing)
// Duration
let durationTitleLabel: UILabel = {
let result = UILabel()
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.text = "ATTACHMENT_INFO_DURATION".localized() + ":"
result.themeTextColor = .textPrimary
return result
}()
let durationContainerStackView: UIStackView = UIStackView(arrangedSubviews: [ durationTitleLabel, durationLabel ])
durationContainerStackView.axis = .vertical
durationContainerStackView.spacing = 6
container.addSubview(durationContainerStackView)
durationContainerStackView.pin(.trailing, to: .trailing, of: container)
durationContainerStackView.pin(.top, to: .bottom, of: fileSizeContainerStackView, withInset: Values.largeSpacing)
durationContainerStackView.set(.width, to: 90)
container.pin(.bottom, to: .bottom, of: durationContainerStackView)
backgroundView.addSubview(container)
container.pin(to: backgroundView, withInset: Values.largeSpacing)
}
// MARK: - Interaction
public func update(attachment: Attachment?) {
guard let attachment: Attachment = attachment else { return }
self.attachment = attachment
fileIdLabel.text = attachment.serverId
fileTypeLabel.text = attachment.contentType
fileSizeLabel.text = OWSFormat.formatFileSize(attachment.byteCount)
resolutionLabel.text = {
guard let width = attachment.width, let height = attachment.height else { return "N/A" }
return "\(width)×\(height)"
}()
durationLabel.text = {
guard let duration = attachment.duration else { return "N/A" }
return floor(duration).formatted(format: .videoDuration)
}()
}
}
}

View File

@ -0,0 +1,62 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionUtilitiesKit
extension MediaInfoVC {
final class MediaPreviewView: UIView {
private static let cornerRadius: CGFloat = 8
private let attachment: Attachment
private let isOutgoing: Bool
// MARK: - UI
private lazy var mediaView: MediaView = {
let result: MediaView = MediaView.init(
attachment: attachment,
isOutgoing: isOutgoing,
cornerRadius: 0
)
return result
}()
// MARK: - Lifecycle
init(attachment: Attachment, isOutgoing: Bool) {
self.attachment = attachment
self.isOutgoing = isOutgoing
super.init(frame: CGRect.zero)
self.accessibilityLabel = "Media info"
setUpViewHierarchy()
}
override init(frame: CGRect) {
preconditionFailure("Use init(attachment:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(attachment:) instead.")
}
private func setUpViewHierarchy() {
set(.width, to: MediaInfoVC.mediaSize)
set(.height, to: MediaInfoVC.mediaSize)
addSubview(mediaView)
mediaView.pin(to: self)
mediaView.loadMedia()
}
// MARK: - Copy
/// This function is used to make sure the carousel view contains this class can loop infinitely
func copyView() -> MediaPreviewView {
return MediaPreviewView(attachment: self.attachment, isOutgoing: self.isOutgoing)
}
}
}

View File

@ -0,0 +1,149 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionUtilitiesKit
final class MediaInfoVC: BaseVC, SessionCarouselViewDelegate {
internal static let mediaSize: CGFloat = UIScreen.main.bounds.width - 2 * Values.veryLargeSpacing
internal static let arrowSize: CGSize = CGSize(width: 20, height: 30)
private let attachments: [Attachment]
private let isOutgoing: Bool
private let threadId: String
private let threadVariant: SessionThread.Variant
private let interactionId: Int64
private var currentPage: Int = 0
// MARK: - UI
private lazy var mediaInfoView: MediaInfoView = MediaInfoView(attachment: nil)
private lazy var mediaCarouselView: SessionCarouselView = {
let slices: [MediaPreviewView] = self.attachments.map {
MediaPreviewView(
attachment: $0,
isOutgoing: self.isOutgoing
)
}
let result: SessionCarouselView = SessionCarouselView(
info: SessionCarouselView.Info(
slices: slices,
copyOfFirstSlice: slices.first?.copyView(),
copyOfLastSlice: slices.last?.copyView(),
sliceSize: CGSize(
width: Self.mediaSize,
height: Self.mediaSize
),
shouldShowPageControl: true,
pageControlStyle: SessionCarouselView.PageControlStyle(
size: .medium,
backgroundColor: .init(white: 0, alpha: 0.4),
bottomInset: Values.mediumSpacing
),
shouldShowArrows: true,
arrowsSize: Self.arrowSize,
cornerRadius: 8
)
)
result.set(.height, to: Self.mediaSize)
result.delegate = self
return result
}()
private lazy var fullScreenButton: UIButton = {
let result: UIButton = UIButton(type: .custom)
result.setImage(
UIImage(systemName: "arrow.up.left.and.arrow.down.right")?
.withRenderingMode(.alwaysTemplate),
for: .normal
)
result.themeTintColor = .textPrimary
result.backgroundColor = .init(white: 0, alpha: 0.4)
result.layer.cornerRadius = 14
result.set(.width, to: 28)
result.set(.height, to: 28)
result.addTarget(self, action: #selector(showMediaFullScreen), for: .touchUpInside)
return result
}()
// MARK: - Initialization
init(
attachments: [Attachment],
isOutgoing: Bool,
threadId: String,
threadVariant: SessionThread.Variant,
interactionId: Int64
) {
self.threadId = threadId
self.threadVariant = threadVariant
self.interactionId = interactionId
self.isOutgoing = isOutgoing
self.attachments = attachments
super.init(nibName: nil, bundle: nil)
}
override init(nibName: String?, bundle: Bundle?) {
preconditionFailure("Use init(attachments:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(attachments:) instead.")
}
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
ViewControllerUtilities.setUpDefaultSessionStyle(
for: self,
title: "message_info_title".localized(),
hasCustomBackButton: false
)
let mediaStackView: UIStackView = UIStackView()
mediaStackView.axis = .horizontal
mediaInfoView.update(attachment: attachments[0])
mediaCarouselView.addSubview(fullScreenButton)
fullScreenButton.pin(.trailing, to: .trailing, of: mediaCarouselView, withInset: -(Values.smallSpacing + Values.veryLargeSpacing))
fullScreenButton.pin(.bottom, to: .bottom, of: mediaCarouselView, withInset: -Values.smallSpacing)
let stackView: UIStackView = UIStackView(arrangedSubviews: [ mediaCarouselView, mediaInfoView ])
stackView.axis = .vertical
stackView.alignment = .center
stackView.spacing = Values.largeSpacing
self.view.addSubview(stackView)
stackView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: self.view)
stackView.pin(.top, to: .top, of: self.view, withInset: Values.veryLargeSpacing)
}
// MARK: - Interaction
@objc func showMediaFullScreen() {
let attachment = self.attachments[self.currentPage]
let viewController: UIViewController? = MediaGalleryViewModel.createDetailViewController(
for: self.threadId,
threadVariant: self.threadVariant,
interactionId: self.interactionId,
selectedAttachmentId: attachment.id,
options: [ .sliderEnabled ]
)
if let viewController: UIViewController = viewController {
viewController.transitioningDelegate = nil
self.present(viewController, animated: true)
}
}
// MARK: - SessionCarouselViewDelegate
func carouselViewDidScrollToNewSlice(currentPage: Int) {
self.currentPage = currentPage
mediaInfoView.update(attachment: attachments[currentPage])
}
}

View File

@ -1,24 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEEzCCAvugAwIBAgIUY9RQqbjhsQEkdeSgV9L0os9xZ7AwDQYJKoZIhvcNAQEL
BQAwfDELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3RvcmlhMRIwEAYDVQQHDAlN
ZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNoIEZvdW5kYXRpb24x
HzAdBgNVBAMMFnB1YmxpYy5sb2tpLmZvdW5kYXRpb24wHhcNMjEwNDA3MDExMDMx
WhcNMjMwNDA3MDExMDMxWjB8MQswCQYDVQQGEwJBVTERMA8GA1UECAwIVmljdG9y
aWExEjAQBgNVBAcMCU1lbGJvdXJuZTElMCMGA1UECgwcT3hlbiBQcml2YWN5IFRl
Y2ggRm91bmRhdGlvbjEfMB0GA1UEAwwWcHVibGljLmxva2kuZm91bmRhdGlvbjCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM5dBJSIR5+VNNUxUOo6FG0e
RmZteRqBt50KXGbOi2A23a6sa57pLFh9Yw3hmlWV+QCL7ipG1X4IC55OStgoesf+
K65VwEMP6Mtq0sSJS3R5TiuV2ZSRdSZTVjUyRXVe5T4Aw6wXVTAbc/HsyS780tDh
GclfDHhonPhZpmTAnSbfMOS+BfOnBNvDxdto0kVh6k5nrGlkT4ECloulHTQF2lwJ
0D6IOtv9AJplPdg6s2c4dY7durOdvr3NNVfvn5PTeRvbEPqzZur4WUUKIPNGu6mY
PxImqd4eUsL0Vod4aAsTIx4YMmCTi0m9W6zJI6nXcK/6a+iiA3+NTNMzEA9gQhEC
AwEAAaOBjDCBiTAdBgNVHQ4EFgQU/zahokxLvvFUpbnM6z/pwS1KsvwwHwYDVR0j
BBgwFoAU/zahokxLvvFUpbnM6z/pwS1KsvwwDwYDVR0TAQH/BAUwAwEB/zAhBgNV
HREEGjAYghZwdWJsaWMubG9raS5mb3VuZGF0aW9uMBMGA1UdJQQMMAoGCCsGAQUF
BwMBMA0GCSqGSIb3DQEBCwUAA4IBAQBql+JvoqpaYrFFTOuDn08U+pdcd3GM7tbI
zRH5LU+YnIpp9aRheek+2COW8DXsIy/kUngETCMLmX6ZaUj/WdHnTDkB0KTgxSHv
ad3ZznKPKZ26qJOklr+0ZWj4J3jHbisSzql6mqq7R2Kp4ESwzwqxvkbykM5RUnmz
Go/3Ol7bpN/ZVwwEkGfD/5rRHf57E/gZn2pBO+zotlQgr7HKRsIXQ2hIXVQqWmPQ
lvfIwrwAZlfES7BARFnHOpyVQxV8uNcV5K5eXzuVFjHBqvq+BtyGhWkP9yKJCHS9
OUXxch0rzRsH2C/kRVVhEk0pI3qlFiRC8pCJs98SNE9l69EQtG7I
-----END CERTIFICATE-----

View File

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEDTCCAvWgAwIBAgIUPwyEuBgX6kfxt+G2tQ4GNTZErMMwDQYJKoZIhvcNAQEL
BQAwejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3RvcmlhMRIwEAYDVQQHDAlN
ZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNoIEZvdW5kYXRpb24x
HTAbBgNVBAMMFHNlZWQxLmdldHNlc3Npb24ub3JnMB4XDTIzMDQxMjEyNTYyMloX
DTI1MDQxMTEyNTYyMlowejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3Rvcmlh
MRIwEAYDVQQHDAlNZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNo
IEZvdW5kYXRpb24xHTAbBgNVBAMMFHNlZWQxLmdldHNlc3Npb24ub3JnMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwxkbApgfFA1upIFj47y+7k+qrM0l
MLDvtX3U95icVgb7HGhxKzkzbCOscKZnVsq1N90drYVh7to0H69b2t6y7l+9q6Zd
Ytzi9U0NoL/OabmR6F+w/XpokRM7CMz9zeg84VLnyu2yRdR26keG4/AZRXk+j8Dy
6xp09+hTF7kfdfzL3HdYyUsyx+/CqoyzU01yn4aVgJ9aufYu38QKnnjfROiVahJf
Xm1MvHLmDCe+WbDFgsp2Y0NjNbpASUgrOEPNnIJeY3Lw4kzwNVGsbSBHgvLgSfaD
p5L6k89TUUKA0onlGFAN/MDXL4DNfjSpmfzHyhM8XwKJ9COSXsvvpX5hHQIDAQAB
o4GKMIGHMB0GA1UdDgQWBBRypjuvZ+5vWDB4kcKE9MkFrVp0tzAfBgNVHSMEGDAW
gBRypjuvZ+5vWDB4kcKE9MkFrVp0tzAPBgNVHRMBAf8EBTADAQH/MB8GA1UdEQQY
MBaCFHNlZWQxLmdldHNlc3Npb24ub3JnMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0G
CSqGSIb3DQEBCwUAA4IBAQBW8q3DzJWVXZew9pJ1MqjqsMuNt2OlnptwIZUme/Lh
krhqBj5o87218542ao1Hkgph4IuuwEQPwJvUoUbh7dT/k+4D6Ua3oUxhmdeyFUv+
mjQKZ1mfcfrwW+6rCWJRa2mAVYfOhdfBQZgLP7NqYdskVQF5LWXSs1IF3XLTyROy
gCeapTexTvKlr/TMW4spE4ewaQ4AfB2c24iVLcpAWT+12GaJ0AYO+gY2o7LQqywN
qIxt2mbvXyf2wuhr489tmGz53mKa3Xu7JC1uU6g9zqJ4FGMYsI8pa0Ec2ODRBb8s
8W54r5LN472aTYn+UGgV8wadzPFd0FZtQABkDTuWSZY7
-----END CERTIFICATE-----

Binary file not shown.

View File

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEDTCCAvWgAwIBAgIUaPiMYcZh7cZZfacCni2NwT5DKh4wDQYJKoZIhvcNAQEL
BQAwejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3RvcmlhMRIwEAYDVQQHDAlN
ZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNoIEZvdW5kYXRpb24x
HTAbBgNVBAMMFHNlZWQyLmdldHNlc3Npb24ub3JnMB4XDTIzMDQxMjEyNTY0NVoX
DTI1MDQxMTEyNTY0NVowejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3Rvcmlh
MRIwEAYDVQQHDAlNZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNo
IEZvdW5kYXRpb24xHTAbBgNVBAMMFHNlZWQyLmdldHNlc3Npb24ub3JnMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAh2UcfW0I+1QWRa3cj7RnMGelYkGK
7l4V6q7je1IkudXBNretkvVF1NCpfZ8dz72JmdGPJ5/uIEW15HDD2L63OmSDVPhA
2JCb/NqmXfeO91lyxgb0sDnN1UH0wzuS75aBjaQ0nXQV3ffmqKnNNv0HK+LTMFD+
Dv2yGDtZTWH6H3VzPLCvHHYXVdyuQHwchAcNQar5k4dbdEIcYIV+ANccPg7iQ81a
ITZ9bCeACdMqbB9gILq21KWdkxCu1fwSXs/B6n+U4UpJyv87fprvAyU3HqQhqlU7
dHnzA1dPn8D4a/3CMYZogVm8USNjv4HmWIwKbYDX+VahvuZwEi6+pwEurQIDAQAB
o4GKMIGHMB0GA1UdDgQWBBRxVM4+gFFipZFAg+Fs4x580js+2TAfBgNVHSMEGDAW
gBRxVM4+gFFipZFAg+Fs4x580js+2TAPBgNVHRMBAf8EBTADAQH/MB8GA1UdEQQY
MBaCFHNlZWQyLmdldHNlc3Npb24ub3JnMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0G
CSqGSIb3DQEBCwUAA4IBAQBIFj6hsOgNVr2kZufimTxoT1TE8uvycIWyt04q6/nP
8h33u/sHuNPdnr2UewqRyDRFefxrGlqBUQAQJVyzJGIlju/HTZaBnVB0H2smCRtK
ZRHAJ/cwcnAp+STjqgPqt1ZZ6JcfFwJZID4pPmrW8WaQNAtQPi2Ly2JLQ+Ym5wus
aGxGjbDRQSWGmUpg5TE+XdDsHeJtCl6HAEjvtXfq1uzKedRzmqYfIa8Rd7b2tmuy
dN27swR4DRJOK4rAxHnI8jt7GKVtPXnYfRuk2+0dVZ4CD6qHw+CO5mcdCabnflgT
XS8BYlOvkAyVbtmZNAacoUZvPRx3o186BMJoK2coQyFN
-----END CERTIFICATE-----

Binary file not shown.

View File

@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIIEDTCCAvWgAwIBAgIULagRXXdxagFp2IRBaWWNeO5dK+IwDQYJKoZIhvcNAQEL
BQAwejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3RvcmlhMRIwEAYDVQQHDAlN
ZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNoIEZvdW5kYXRpb24x
HTAbBgNVBAMMFHNlZWQzLmdldHNlc3Npb24ub3JnMB4XDTIzMDQxMjEyNTY1M1oX
DTI1MDQxMTEyNTY1M1owejELMAkGA1UEBhMCQVUxETAPBgNVBAgMCFZpY3Rvcmlh
MRIwEAYDVQQHDAlNZWxib3VybmUxJTAjBgNVBAoMHE94ZW4gUHJpdmFjeSBUZWNo
IEZvdW5kYXRpb24xHTAbBgNVBAMMFHNlZWQzLmdldHNlc3Npb24ub3JnMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA23lBHUMU8xl3ZBPhQJuupNk9pqAW
8UvqyMX2BYWVc6bGpgRiqnf2Rc58Ol9jSM4VT29jXHD+PXXQLIvoZmni/5fbdkZl
zFAvnPFoWf4g4xCdREEpJ7m/sWh8aG6Bf7Eh+sTP6qaspJUPo5q4ovUd4tUoTt7f
bVlnzncXI1z2bhrmxWR8ahl9SwMjd/qKZMFKL3o12f4xhYu0Jfp1aFeKdrRImfZR
X6hzXM6uUe5X+/3mrmKvYCVnNoNCwsdyxTZp4JYXCqhG/g29CbWDFTTqxWVXySFK
+mujbHfWIBvRheYvO9x7Wb2jsPq5VbyP1MoqxPThKjF+FeCfU7X0+Fy+3QIDAQAB
o4GKMIGHMB0GA1UdDgQWBBRXwt1MJe73lcOBv+JHmjqWyypB2DAfBgNVHSMEGDAW
gBRXwt1MJe73lcOBv+JHmjqWyypB2DAPBgNVHRMBAf8EBTADAQH/MB8GA1UdEQQY
MBaCFHNlZWQzLmdldHNlc3Npb24ub3JnMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0G
CSqGSIb3DQEBCwUAA4IBAQAb+5FUjLXfgF0QmeBJrpC4B+3gIyw6QGTnbMXM5zVt
zKANoZxeQesZXkSGDTlszI4XnBs/bDzf87AROxDuT0guxt33+PhyXNw+9FdV3CAG
t/8FyRMPyJI8xog0mlPgjVqSw2PGjXtj2uVEkB7gkm6+AoPUfZYdPOplezrpvRES
tMVbjsxxiMiOQAOm1bS69dC16xQ6bZ8++QNZXPhj9o1a+tQCb71Bp2sYI66hCfmy
DRSJEDW7fCPb/da1D8cN68qr5vxIJjm5cWaF4xlN9pc9pywssTbPYhPSluravRDg
qyqfraj2YhdDNOSRj/U6IuYbL+jKWuaTcrEFYyNExxkq
-----END CERTIFICATE-----

Binary file not shown.

View File

@ -1,25 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEITCCAwmgAwIBAgIUJsox1ZQPK/6iDsCC+MUJfNAlFuYwDQYJKoZIhvcNAQEL
BQAwgYAxCzAJBgNVBAYTAkFVMREwDwYDVQQIDAhWaWN0b3JpYTESMBAGA1UEBwwJ
TWVsYm91cm5lMSUwIwYDVQQKDBxPeGVuIFByaXZhY3kgVGVjaCBGb3VuZGF0aW9u
MSMwIQYDVQQDDBpzdG9yYWdlLnNlZWQxLmxva2kubmV0d29yazAeFw0yMTA0MDcw
MTE5MjZaFw0yMzA0MDcwMTE5MjZaMIGAMQswCQYDVQQGEwJBVTERMA8GA1UECAwI
VmljdG9yaWExEjAQBgNVBAcMCU1lbGJvdXJuZTElMCMGA1UECgwcT3hlbiBQcml2
YWN5IFRlY2ggRm91bmRhdGlvbjEjMCEGA1UEAwwac3RvcmFnZS5zZWVkMS5sb2tp
Lm5ldHdvcmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtWH3Rz8Dd
kEmM7tcBWHrJ/G8drr/+qidboEVYzxpyRjszaDxKXVhx4eBBsAD5RuCWuTuZmM8k
TKEDLtf8xfb5SQ7YNX+346s9NXS5Poy4CIPASiW/QWXgIHFbVdv2hC+cKOP61OLM
OGnOxfig6tQyd6EaCkedpY1DvSa2lPnQSOwC/jXCx6Vboc0zTY5R2bHtNc9hjIFP
F4VClLAQSh2F4R1V9MH5KZMW+CCP6oaJY658W9JYXYRwlLrL2EFOVxHgcxq/6+fw
+axXK9OXJrGZjuA+hiz+L/uAOtE4WuxrSeuNMHSrMtM9QqVn4bBuMJ21mAzfNoMP
OIwgMT9DwUjVAgMBAAGjgZAwgY0wHQYDVR0OBBYEFOubJp9SoXIw+ONiWgkOaW8K
zI/TMB8GA1UdIwQYMBaAFOubJp9SoXIw+ONiWgkOaW8KzI/TMA8GA1UdEwEB/wQF
MAMBAf8wJQYDVR0RBB4wHIIac3RvcmFnZS5zZWVkMS5sb2tpLm5ldHdvcmswEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggEBAIiHNhNrjYvwXVWs
gacx8T/dpqpu9GE3L17LotgQr4R+IYHpNtcmwOTdtWWFfUTr75OCs+c3DqgRKEoj
lnULOsVcalpAGIvW15/fmZWOf66Dpa4+ljDmAc3SOQiD0gGNtqblgI5zG1HF38QP
hjYRhCZ5CVeGOLucvQ8tVVwQvArPFIkBr0jH9jHVgRWEI2MeI3FsU2H93D4TfGln
N4SmmCfYBqygaaZBWkJEt0bYhn8uGHdU9UY9L2FPtfHVKkmFgO7cASGlvXS7B/TT
/8IgbtM3O8mZc2asmdQhGwoAKz93ryyCd8X2UZJg/IwCSCayOlYZWY2fR4OPQmmV
gxJsm+g=
-----END CERTIFICATE-----

View File

@ -1,25 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEITCCAwmgAwIBAgIUc486Dy9Y00bUFfDeYmJIgSS5xREwDQYJKoZIhvcNAQEL
BQAwgYAxCzAJBgNVBAYTAkFVMREwDwYDVQQIDAhWaWN0b3JpYTESMBAGA1UEBwwJ
TWVsYm91cm5lMSUwIwYDVQQKDBxPeGVuIFByaXZhY3kgVGVjaCBGb3VuZGF0aW9u
MSMwIQYDVQQDDBpzdG9yYWdlLnNlZWQzLmxva2kubmV0d29yazAeFw0yMTA0MDcw
MTIwNTJaFw0yMzA0MDcwMTIwNTJaMIGAMQswCQYDVQQGEwJBVTERMA8GA1UECAwI
VmljdG9yaWExEjAQBgNVBAcMCU1lbGJvdXJuZTElMCMGA1UECgwcT3hlbiBQcml2
YWN5IFRlY2ggRm91bmRhdGlvbjEjMCEGA1UEAwwac3RvcmFnZS5zZWVkMy5sb2tp
Lm5ldHdvcmswggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtokMlsFzf
piYeD0EVNikMyvjltpF6fUEde9NOVrTtNTQT6kkDk+/0HF5LYgPaatv6v7fpUQHi
kIwd6F0LTRGeWDFdsaWMdtlR1n/GxLPrOROsE8dcLt6GLavPf9rDabgva93m/JD6
XW+Ne+MPEwqS8dAmFGhZd0gju6AtKFoSHnIf5pSQN6fSZUF/JQtHLVprAKKWKDiS
ZwmWbmrZR2aofLD/VRpetabajnZlv9EeWloQwvUsw1C1hkAmmtFeeXtg7ePwrOzo
6CnmcUJwOmi+LWqQV4A+58RZPFKaZoC5pzaKd0OYB8eZ8HB1F41UjGJgheX5Cyl4
+amfF3l8dSq1AgMBAAGjgZAwgY0wHQYDVR0OBBYEFM9VSq4pGydjtX92Beul4+ml
jBKtMB8GA1UdIwQYMBaAFM9VSq4pGydjtX92Beul4+mljBKtMA8GA1UdEwEB/wQF
MAMBAf8wJQYDVR0RBB4wHIIac3RvcmFnZS5zZWVkMy5sb2tpLm5ldHdvcmswEwYD
VR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggEBAAYxmhhkcKE1n6g1
JqOa3UCBo4EfbqY5+FDZ0FVqv/cwemwVpKLbe6luRIS8poomdPCyMOS45V7wN3H9
cFpfJ1TW19ydPVKmCXrl29ngmnY1q7YDwE/4qi3VK/UiqDkTHMKWjVPkenOyi8u6
VVQANXSnKrn6GtigNFjGyD38O+j7AUSXBtXOJczaoF6r6BWgwQZ2WmgjuwvKTWSN
4r8uObERoAQYVaeXfgdr4e9X/JdskBDaLFfoW/rrSozHB4FqVNFW96k+aIUgRa5p
9kv115QcBPCSh9qOyTHij4tswS6SyOFaiKrNC4hgHQXP4QgioKmtsR/2Y+qJ6ddH
6oo+4QU=
-----END CERTIFICATE-----

View File

@ -66,17 +66,17 @@
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>public.loki.foundation</key>
<key>seed1.getsession.org</key>
<dict>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
<key>storage.seed1.loki.network</key>
<key>seed2.getsession.org</key>
<dict>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
<key>storage.seed3.loki.network</key>
<key>seed3.getsession.org</key>
<dict>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Nur für mich löschen";
"delete_message_for_everyone" = "Für jeden löschen";
"delete_message_for_me_and_recipient" = "Für mich und %@ löschen";
"context_menu_info" = "Info";
"context_menu_reply" = "Antworten";
"context_menu_save" = "Speichern";
"context_menu_ban_user" = "Nutzer sperren";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Eliminar solo para mí";
"delete_message_for_everyone" = "Eliminar para todos";
"delete_message_for_me_and_recipient" = "Eliminar para mí y para %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Responder";
"context_menu_save" = "Guardar";
"context_menu_ban_user" = "Banear Usuario";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "حذف برای من";
"delete_message_for_everyone" = "حذف برای همه";
"delete_message_for_me_and_recipient" = "حذف برای من و %@";
"context_menu_info" = "Info";
"context_menu_reply" = "پاسخ";
"context_menu_save" = "ذخیره";
"context_menu_ban_user" = "مسدود کردن کاربر";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Poista vain minun nähtäväksi";
"delete_message_for_everyone" = "Poista kaikkien näkyviltä";
"delete_message_for_me_and_recipient" = "Poista minulta ja vastaanottajalta";
"context_menu_info" = "Info";
"context_menu_reply" = "Vastaa";
"context_menu_save" = "Tallenna";
"context_menu_ban_user" = "Estä Käyttäjä";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Supprimer pour moi uniquement";
"delete_message_for_everyone" = "Supprimer pour tout le monde";
"delete_message_for_me_and_recipient" = "Supprimer pour moi et %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Répondre";
"context_menu_save" = "Enregistrer";
"context_menu_ban_user" = "Bannir l'utilisateur";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Izbriši samo za mene";
"delete_message_for_everyone" = "Izbriši za sve";
"delete_message_for_me_and_recipient" = "Izbriši za mene i %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Odgovori";
"context_menu_save" = "Spremi";
"context_menu_ban_user" = "Zabrani korisnik";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Elimina solo per me";
"delete_message_for_everyone" = "Elimina per tutti";
"delete_message_for_me_and_recipient" = "Elimina per me e %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Rispondi";
"context_menu_save" = "Salva";
"context_menu_ban_user" = "Banna utente";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "自分の端末から削除";
"delete_message_for_everyone" = "全員の端末から削除";
"delete_message_for_me_and_recipient" = "自分と %@ の端末から削除する";
"context_menu_info" = "Info";
"context_menu_reply" = "返信";
"context_menu_save" = "保存";
"context_menu_ban_user" = "ユーザーをBAN";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Verwijder alleen voor mij";
"delete_message_for_everyone" = "Verwijder voor iedereen";
"delete_message_for_me_and_recipient" = "Verwijderen voor mij en %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Antwoord";
"context_menu_save" = "Opslaan";
"context_menu_ban_user" = "Gebruiker verbannen";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,11 +366,12 @@
"delete_message_for_me" = "Usuń tylko dla mnie";
"delete_message_for_everyone" = "Usuń dla wszystkich";
"delete_message_for_me_and_recipient" = "Usuń dla mnie i %@";
"context_menu_ban_user_error_alert_message" = "Unable to ban user";
"context_menu_info" = "Info";
"context_menu_reply" = "Odpowiedz";
"context_menu_save" = "Zapisz";
"context_menu_ban_user" = "Zbanuj użytkownika";
"context_menu_ban_and_delete_all" = "Zbanuj i usuń wszystko";
"context_menu_ban_user_error_alert_message" = "Unable to ban user";
"accessibility_expanding_attachments_button" = "Dodaj załączniki";
"accessibility_gif_button" = "Gif";
"accessibility_document_button" = "Dokument";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Apagar para mim";
"delete_message_for_everyone" = "Apagar para todos";
"delete_message_for_me_and_recipient" = "Apagar para mim e para %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Responder";
"context_menu_save" = "Salvar";
"context_menu_ban_user" = "Banir Usuário";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Удалить только для меня";
"delete_message_for_everyone" = "Удалить для всех";
"delete_message_for_me_and_recipient" = "Удалить для меня и %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Ответить";
"context_menu_save" = "Сохранить";
"context_menu_ban_user" = "Заблокировать пользователя";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_info" = "Info";
"context_menu_reply" = "පිළිතුරු";
"context_menu_save" = "සුරකින්න";
"context_menu_ban_user" = "Ban User";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Vymazať len u mňa";
"delete_message_for_everyone" = "Vymazať u všetkých";
"delete_message_for_me_and_recipient" = "Vymazať pre mňa a %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Odpovedať";
"context_menu_save" = "Uložiť";
"context_menu_ban_user" = "Zablokovanie používateľa";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Reply";
"context_menu_save" = "Spara";
"context_menu_ban_user" = "Ban User";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "Delete just for me";
"delete_message_for_everyone" = "Delete for everyone";
"delete_message_for_me_and_recipient" = "Delete for me and %@";
"context_menu_info" = "Info";
"context_menu_reply" = "Reply";
"context_menu_save" = "Save";
"context_menu_ban_user" = "Ban User";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "只為我自己刪除";
"delete_message_for_everyone" = "從所有人的裝置上刪除";
"delete_message_for_me_and_recipient" = "為我和 %@ 刪除";
"context_menu_info" = "Info";
"context_menu_reply" = "回覆";
"context_menu_save" = "儲存";
"context_menu_ban_user" = "封鎖用戶";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -366,6 +366,7 @@
"delete_message_for_me" = "仅为我删除";
"delete_message_for_everyone" = "为所有人删除";
"delete_message_for_me_and_recipient" = "为我和 %@ 删除";
"context_menu_info" = "Info";
"context_menu_reply" = "回复";
"context_menu_save" = "保存";
"context_menu_ban_user" = "封禁用户";
@ -592,6 +593,14 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_FROM" = "From";
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
"ATTACHMENT_INFO_DURATION" = "Duration";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
@ -601,6 +610,7 @@
"context_menu_resync" = "Resync";
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"message_info_title" = "Message Info";
"mute_button_text" = "Mute";
"unmute_button_text" = "Unmute";
"mark_read_button_text" = "Mark read";

View File

@ -0,0 +1,72 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionUtilitiesKit
extension SessionCarouselView {
public struct Info {
let slices: [UIView]
let copyOfFirstSlice: UIView?
let copyOfLastSlice: UIView?
let sliceSize: CGSize
let sliceCount: Int
let shouldShowPageControl: Bool
let pageControlStyle: PageControlStyle
let shouldShowArrows: Bool
let arrowsSize: CGSize
let cornerRadius: CGFloat
// MARK: - Initialization
init(
slices: [UIView] = [],
copyOfFirstSlice: UIView? = nil,
copyOfLastSlice: UIView? = nil,
sliceSize: CGSize = .zero,
shouldShowPageControl: Bool = true,
pageControlStyle: PageControlStyle,
shouldShowArrows: Bool = true,
arrowsSize: CGSize = .zero,
cornerRadius: CGFloat = 0
) {
self.slices = slices
self.copyOfFirstSlice = copyOfFirstSlice
self.copyOfLastSlice = copyOfLastSlice
self.sliceSize = sliceSize
self.sliceCount = slices.count
self.shouldShowPageControl = shouldShowPageControl && (self.sliceCount > 1)
self.pageControlStyle = pageControlStyle
self.shouldShowArrows = shouldShowArrows && (self.sliceCount > 1)
self.arrowsSize = arrowsSize
self.cornerRadius = cornerRadius
}
}
public struct PageControlStyle {
enum DotSize: CGFloat {
case mini = 0.5
case medium = 0.8
case original = 1
}
let height: CGFloat?
let size: DotSize
let backgroundColor: UIColor
let bottomInset: CGFloat
// MARK: - Initialization
init(
height: CGFloat? = nil,
size: DotSize = .original,
backgroundColor: UIColor = .clear,
bottomInset: CGFloat = 0
) {
self.height = height
self.size = size
self.backgroundColor = backgroundColor
self.bottomInset = bottomInset
}
}
}

View File

@ -0,0 +1,202 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionUtilitiesKit
final class SessionCarouselView: UIView, UIScrollViewDelegate {
private let slicesForLoop: [UIView]
private let info: SessionCarouselView.Info
var delegate: SessionCarouselViewDelegate?
// MARK: - UI
private lazy var scrollView: UIScrollView = {
let result: UIScrollView = UIScrollView()
result.delegate = self
result.isPagingEnabled = true
result.showsHorizontalScrollIndicator = false
result.showsVerticalScrollIndicator = false
result.contentSize = CGSize(
width: self.info.sliceSize.width * CGFloat(self.slicesForLoop.count),
height: self.info.sliceSize.height
)
result.layer.cornerRadius = self.info.cornerRadius
result.layer.masksToBounds = true
return result
}()
private lazy var pageControl: UIPageControl = {
let result: UIPageControl = UIPageControl()
result.numberOfPages = self.info.sliceCount
result.currentPage = 0
result.isHidden = !self.info.shouldShowPageControl
result.transform = CGAffineTransform(
scaleX: self.info.pageControlStyle.size.rawValue,
y: self.info.pageControlStyle.size.rawValue
)
return result
}()
private lazy var arrowLeft: UIButton = {
let result = UIButton(type: .custom)
result.setImage(UIImage(systemName: "chevron.left")?.withRenderingMode(.alwaysTemplate), for: .normal)
result.addTarget(self, action: #selector(scrollToPreviousSlice), for: .touchUpInside)
result.themeTintColor = .textPrimary
result.set(.width, to: self.info.arrowsSize.width)
result.set(.height, to: self.info.arrowsSize.height)
result.isHidden = !self.info.shouldShowArrows
return result
}()
private lazy var arrowRight: UIButton = {
let result = UIButton(type: .custom)
result.setImage(UIImage(systemName: "chevron.right")?.withRenderingMode(.alwaysTemplate), for: .normal)
result.addTarget(self, action: #selector(scrollToNextSlice), for: .touchUpInside)
result.themeTintColor = .textPrimary
result.set(.width, to: self.info.arrowsSize.width)
result.set(.height, to: self.info.arrowsSize.height)
result.isHidden = !self.info.shouldShowArrows
return result
}()
// MARK: - Lifecycle
init(info: SessionCarouselView.Info) {
self.info = info
if self.info.sliceCount > 1,
let copyOfFirstSlice: UIView = self.info.copyOfFirstSlice,
let copyOfLastSlice: UIView = self.info.copyOfLastSlice
{
self.slicesForLoop = [copyOfLastSlice]
.appending(contentsOf: self.info.slices)
.appending(copyOfFirstSlice)
} else {
self.slicesForLoop = self.info.slices
}
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
override init(frame: CGRect) {
preconditionFailure("Use init(attachment:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(attachment:) instead.")
}
private func setUpViewHierarchy() {
set(.width, to: self.info.sliceSize.width + Values.largeSpacing + 2 * self.info.arrowsSize.width)
set(.height, to: self.info.sliceSize.height)
let stackView: UIStackView = UIStackView(arrangedSubviews: self.slicesForLoop)
stackView.axis = .horizontal
stackView.set(.width, to: self.info.sliceSize.width * CGFloat(self.slicesForLoop.count))
stackView.set(.height, to: self.info.sliceSize.height)
addSubview(self.scrollView)
scrollView.center(in: self)
scrollView.set(.width, to: self.info.sliceSize.width)
scrollView.set(.height, to: self.info.sliceSize.height)
scrollView.addSubview(stackView)
scrollView.setContentOffset(
CGPoint(
x: Int(self.info.sliceSize.width) * (self.info.sliceCount > 1 ? 1 : 0),
y: 0
),
animated: false
)
addSubview(self.pageControl)
self.pageControl.center(.horizontal, in: self)
self.pageControl.pin(.bottom, to: .bottom, of: self)
addSubview(self.arrowLeft)
self.arrowLeft.pin(.leading, to: .leading, of: self)
self.arrowLeft.center(.vertical, in: self)
addSubview(self.arrowRight)
self.arrowRight.pin(.trailing, to: .trailing, of: self)
self.arrowRight.center(.vertical, in: self)
}
// MARK: - UIScrollViewDelegate
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let pageIndex: Int = {
let maybeCurrentPageIndex: Int = Int(round(scrollView.contentOffset.x/self.info.sliceSize.width))
if self.info.sliceCount > 1 {
if maybeCurrentPageIndex == 0 {
return pageControl.numberOfPages - 1
}
if maybeCurrentPageIndex == self.slicesForLoop.count - 1 {
return 0
}
return maybeCurrentPageIndex - 1
}
return maybeCurrentPageIndex
}()
pageControl.currentPage = pageIndex
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
setCorrectCotentOffsetIfNeeded(scrollView)
delegate?.carouselViewDidScrollToNewSlice(currentPage: pageControl.currentPage)
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
setCorrectCotentOffsetIfNeeded(scrollView)
delegate?.carouselViewDidScrollToNewSlice(currentPage: pageControl.currentPage)
}
private func setCorrectCotentOffsetIfNeeded(_ scrollView: UIScrollView) {
if pageControl.currentPage == 0 {
scrollView.setContentOffset(
CGPoint(
x: Int(self.info.sliceSize.width) * 1,
y: 0
),
animated: false
)
}
if pageControl.currentPage == pageControl.numberOfPages - 1 {
let realLastIndex: Int = self.slicesForLoop.count - 2
scrollView.setContentOffset(
CGPoint(
x: Int(self.info.sliceSize.width) * realLastIndex,
y: 0
),
animated: false
)
}
}
// MARK: - Interaction
@objc func scrollToNextSlice() {
self.scrollView.setContentOffset(
CGPoint(
x: self.scrollView.contentOffset.x + self.info.sliceSize.width,
y: 0
),
animated: true
)
}
@objc func scrollToPreviousSlice() {
self.scrollView.setContentOffset(
CGPoint(
x: self.scrollView.contentOffset.x - self.info.sliceSize.width,
y: 0
),
animated: true
)
}
}

View File

@ -0,0 +1,7 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
public protocol SessionCarouselViewDelegate: AnyObject {
func carouselViewDidScrollToNewSlice(currentPage: Int)
}

View File

@ -29,6 +29,14 @@ public extension Date {
return "DATE_NOW".localized()
}
var fromattedForMessageInfo: String {
let formatter: DateFormatter = DateFormatter()
formatter.locale = Locale.current
formatter.dateFormat = "h:mm a EEE, DD/MM/YYYY"
return formatter.string(from: self)
}
}
// MARK: - Formatters

View File

@ -40,7 +40,7 @@ final class IP2Country {
private func cacheCountry(for ip: String) -> String {
if let result = countryNamesCache[ip] { return result }
let ipAsInt = IPv4.toInt(ip)
guard let ipv4TableIndex = given(ipv4Table["network"]!.firstIndex(where: { $0 > ipAsInt }), { $0 - 1 }) else { return "Unknown Country" } // Relies on the array being sorted
guard let ipv4TableIndex = ipv4Table["network"]!.firstIndex(where: { $0 > ipAsInt }).map({ $0 - 1 }) else { return "Unknown Country" } // Relies on the array being sorted
let countryID = ipv4Table["registered_country_geoname_id"]![ipv4TableIndex]
guard let countryNamesTableIndex = countryNamesTable["geoname_id"]!.firstIndex(of: String(countryID)) else { return "Unknown Country" }
let result = countryNamesTable["country_name"]![countryNamesTableIndex]

View File

@ -6,6 +6,10 @@ extension WebRTCSession {
localVideoTrack.add(renderer)
}
public func removeLocalRenderer(_ renderer: RTCVideoRenderer) {
localVideoTrack.remove(renderer)
}
public func attachRemoteRenderer(_ renderer: RTCVideoRenderer) {
remoteVideoTrack?.add(renderer)
}

View File

@ -493,6 +493,7 @@ extension Attachment {
public let interactionId: Int64
public let state: Attachment.State
public let downloadUrl: String?
public let albumIndex: Int
}
public static func stateInfo(authorId: String, state: State? = nil) -> SQLRequest<Attachment.StateInfo> {
@ -510,7 +511,8 @@ extension Attachment {
\(attachment[.id]) AS attachmentId,
\(interaction[.id]) AS interactionId,
\(attachment[.state]) AS state,
\(attachment[.downloadUrl]) AS downloadUrl
\(attachment[.downloadUrl]) AS downloadUrl,
IFNULL(\(interactionAttachment[.albumIndex]), 0) AS albumIndex
FROM \(Attachment.self)
@ -520,8 +522,7 @@ extension Attachment {
\(interaction[.id]) = \(interactionAttachment[.interactionId]) OR
(
\(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND
/* Note: This equation MUST match the `linkPreviewFilterLiteral` logic in Interaction.swift */
(ROUND((\(interaction[.timestampMs]) / 1000 / 100000) - 0.5) * 100000) = \(linkPreview[.timestamp])
\(Interaction.linkPreviewFilterLiteral)
)
)
@ -556,7 +557,8 @@ extension Attachment {
\(attachment[.id]) AS attachmentId,
\(interaction[.id]) AS interactionId,
\(attachment[.state]) AS state,
\(attachment[.downloadUrl]) AS downloadUrl
\(attachment[.downloadUrl]) AS downloadUrl,
IFNULL(\(interactionAttachment[.albumIndex]), 0) AS albumIndex
FROM \(Attachment.self)
@ -566,8 +568,7 @@ extension Attachment {
\(interaction[.id]) = \(interactionAttachment[.interactionId]) OR
(
\(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND
/* Note: This equation MUST match the `linkPreviewFilterLiteral` logic in Interaction.swift */
(ROUND((\(interaction[.timestampMs]) / 1000 / 100000) - 0.5) * 100000) = \(linkPreview[.timestamp])
\(Interaction.linkPreviewFilterLiteral)
)
)

View File

@ -29,13 +29,13 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
/// Whenever using this `linkPreview` association make sure to filter the result using
/// `.filter(literal: Interaction.linkPreviewFilterLiteral)` to ensure the correct LinkPreview is returned
public static let linkPreview = hasOne(LinkPreview.self, using: LinkPreview.interactionForeignKey)
public static func linkPreviewFilterLiteral(
timestampColumn: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
) -> SQL {
public static var linkPreviewFilterLiteral: SQL = {
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
return "(ROUND((\(Interaction.self).\(timestampColumn) / 1000 / 100000) - 0.5) * 100000) = \(linkPreview[.timestamp])"
}
let halfResolution: Double = LinkPreview.timstampResolution
return "(\(interaction[.timestampMs]) BETWEEN (\(linkPreview[.timestamp]) - \(halfResolution)) AND (\(linkPreview[.timestamp]) + \(halfResolution)))"
}()
public static let recipientStates = hasMany(RecipientState.self, using: RecipientState.interactionForeignKey)
public typealias Columns = CodingKeys
@ -250,10 +250,14 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
public var linkPreview: QueryInterfaceRequest<LinkPreview> {
/// **Note:** This equation **MUST** match the `linkPreviewFilterLiteral` logic
let halfResolution: Double = LinkPreview.timstampResolution
let roundedTimestamp: Double = (round(((Double(timestampMs) / 1000) / 100000) - 0.5) * 100000)
return request(for: Interaction.linkPreview)
.filter(LinkPreview.Columns.timestamp == roundedTimestamp)
.filter(
(Interaction.Columns.timestampMs >= (LinkPreview.Columns.timestamp - halfResolution)) &&
(Interaction.Columns.timestampMs <= (LinkPreview.Columns.timestamp + halfResolution))
)
}
public var recipientStates: QueryInterfaceRequest<RecipientState> {

View File

@ -18,8 +18,9 @@ public enum FileServerAPI {
/// exactly will be fine but a single byte more will result in an error
public static let maxFileSize = 10_000_000
/// Standard timeout is 10 seconds which is a little too short fir file upload/download with slightly larger files
public static let fileTimeout: TimeInterval = 30
/// Standard timeout is 10 seconds which is a little too short for file upload/download with slightly larger files
public static let fileDownloadTimeout: TimeInterval = 30
public static let fileUploadTimeout: TimeInterval = 60
// MARK: - File Storage
@ -35,7 +36,7 @@ public enum FileServerAPI {
body: Array(file)
)
return send(request, serverPublicKey: serverPublicKey)
return send(request, serverPublicKey: serverPublicKey, timeout: FileServerAPI.fileUploadTimeout)
.decoded(as: FileUploadResponse.self)
}
@ -46,7 +47,7 @@ public enum FileServerAPI {
endpoint: .fileIndividual(fileId: fileId)
)
return send(request, serverPublicKey: serverPublicKey)
return send(request, serverPublicKey: serverPublicKey, timeout: FileServerAPI.fileDownloadTimeout)
}
public static func getVersion(_ platform: String) -> AnyPublisher<String, Error> {
@ -58,7 +59,7 @@ public enum FileServerAPI {
]
)
return send(request, serverPublicKey: serverPublicKey)
return send(request, serverPublicKey: serverPublicKey, timeout: HTTP.timeout)
.decoded(as: VersionResponse.self)
.map { response in response.version }
.eraseToAnyPublisher()
@ -68,7 +69,8 @@ public enum FileServerAPI {
private static func send<T: Encodable>(
_ request: Request<T, Endpoint>,
serverPublicKey: String
serverPublicKey: String,
timeout: TimeInterval
) -> AnyPublisher<Data, Error> {
let urlRequest: URLRequest
@ -80,7 +82,13 @@ public enum FileServerAPI {
.eraseToAnyPublisher()
}
return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, with: serverPublicKey, timeout: FileServerAPI.fileTimeout)
return OnionRequestAPI
.sendOnionRequest(
urlRequest,
to: request.server,
with: serverPublicKey,
timeout: timeout
)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.tryMap { _, response -> Data in
guard let response: Data = response else { throw HTTPError.parsingFailed }

View File

@ -142,7 +142,7 @@ public enum GarbageCollectionJob: JobExecutor {
FROM \(LinkPreview.self)
LEFT JOIN \(Interaction.self) ON (
\(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND
\(Interaction.linkPreviewFilterLiteral())
\(Interaction.linkPreviewFilterLiteral)
)
WHERE \(interaction[.id]) IS NULL
)

View File

@ -57,6 +57,7 @@ public enum MessageSendJob: JobExecutor {
.stateInfo(interactionId: interactionId)
.fetchAll(db)
let maybeFileIds: [String?] = allAttachmentStateInfo
.sorted { lhs, rhs in lhs.albumIndex < rhs.albumIndex }
.map { Attachment.fileId(for: $0.downloadUrl) }
let fileIds: [String] = maybeFileIds.compactMap { $0 }

View File

@ -160,8 +160,15 @@ public final class VisibleMessage: Message {
// Attachments
let attachments: [Attachment]? = try? Attachment.fetchAll(db, ids: self.attachmentIds)
let attachmentProtos = (attachments ?? []).compactMap { $0.buildProto() }
let attachmentIdIndexes: [String: Int] = (try? InteractionAttachment
.filter(self.attachmentIds.contains(InteractionAttachment.Columns.attachmentId))
.fetchAll(db))
.defaulting(to: [])
.reduce(into: [:]) { result, next in result[next.attachmentId] = next.albumIndex }
let attachments: [Attachment] = (try? Attachment.fetchAll(db, ids: self.attachmentIds))
.defaulting(to: [])
.sorted { lhs, rhs in (attachmentIdIndexes[lhs.id] ?? 0) < (attachmentIdIndexes[rhs.id] ?? 0) }
let attachmentProtos = attachments.compactMap { $0.buildProto() }
dataMessage.setAttachments(attachmentProtos)
// Open group invitation

View File

@ -922,7 +922,7 @@ public enum OpenGroupAPI {
],
body: bytes
),
timeout: FileServerAPI.fileTimeout,
timeout: FileServerAPI.fileUploadTimeout,
using: dependencies
)
.decoded(as: FileUploadResponse.self, using: dependencies)
@ -944,7 +944,7 @@ public enum OpenGroupAPI {
server: server,
endpoint: .roomFileIndividual(roomToken, fileId)
),
timeout: FileServerAPI.fileTimeout,
timeout: FileServerAPI.fileDownloadTimeout,
using: dependencies
)
.tryMap { responseInfo, maybeData -> (ResponseInfoType, Data) in

View File

@ -74,6 +74,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
public let id: Int64
public let variant: Interaction.Variant
public let timestampMs: Int64
public let receivedAtTimestampMs: Int64
public let authorId: String
private let authorNameInternal: String?
public let body: String?
@ -123,6 +124,9 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
/// This value will be used to populate the Context Menu and date header (if present)
public var dateForUI: Date { Date(timeIntervalSince1970: (TimeInterval(self.timestampMs) / 1000)) }
/// This value will be used to populate the Message Info (if present)
public var receivedDateForUI: Date { Date(timeIntervalSince1970: (TimeInterval(self.receivedAtTimestampMs) / 1000)) }
/// This value specifies whether the body contains only emoji characters
public let containsOnlyEmoji: Bool?
@ -164,6 +168,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
id: self.id,
variant: self.variant,
timestampMs: self.timestampMs,
receivedAtTimestampMs: self.receivedAtTimestampMs,
authorId: self.authorId,
authorNameInternal: self.authorNameInternal,
body: self.body,
@ -326,6 +331,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
id: self.id,
variant: self.variant,
timestampMs: self.timestampMs,
receivedAtTimestampMs: self.receivedAtTimestampMs,
authorId: self.authorId,
authorNameInternal: self.authorNameInternal,
body: (!self.variant.isInfoMessage ?
@ -503,6 +509,7 @@ public extension MessageViewModel {
init(
variant: Interaction.Variant = .standardOutgoing,
timestampMs: Int64 = Int64.max,
receivedAtTimestampMs: Int64 = Int64.max,
body: String? = nil,
quote: Quote? = nil,
cellType: CellType = .typingIndicator,
@ -530,6 +537,7 @@ public extension MessageViewModel {
self.id = targetId
self.variant = variant
self.timestampMs = timestampMs
self.receivedAtTimestampMs = receivedAtTimestampMs
self.authorId = ""
self.authorNameInternal = nil
self.body = body
@ -640,29 +648,35 @@ public extension MessageViewModel {
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias()
let recipientState: TypedTableAlias<RecipientState> = TypedTableAlias()
let contact: TypedTableAlias<Contact> = TypedTableAlias()
let disappearingMessagesConfig: TypedTableAlias<DisappearingMessagesConfiguration> = TypedTableAlias()
let profile: TypedTableAlias<Profile> = TypedTableAlias()
let quote: TypedTableAlias<Quote> = TypedTableAlias()
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
let threadProfileTableLiteral: SQL = SQL(stringLiteral: "threadProfile")
let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name)
let profileNicknameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.nickname.name)
let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name)
let interactionStateInteractionIdColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name)
let readReceiptTableLiteral: SQL = SQL(stringLiteral: "readReceipt")
let readReceiptReadTimestampMsColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name)
let attachmentIdColumnLiteral: SQL = SQL(stringLiteral: Attachment.Columns.id.name)
let groupMemberModeratorTableLiteral: SQL = SQL(stringLiteral: "groupMemberModerator")
let groupMemberAdminTableLiteral: SQL = SQL(stringLiteral: "groupMemberAdmin")
let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name)
let groupMemberProfileIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.profileId.name)
let groupMemberRoleColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.role.name)
let threadProfile: SQL = SQL(stringLiteral: "threadProfile")
let quoteInteraction: SQL = SQL(stringLiteral: "quoteInteraction")
let quoteInteractionAttachment: SQL = SQL(stringLiteral: "quoteInteractionAttachment")
let readReceipt: SQL = SQL(stringLiteral: "readReceipt")
let idColumn: SQL = SQL(stringLiteral: Interaction.Columns.id.name)
let interactionBodyColumn: SQL = SQL(stringLiteral: Interaction.Columns.body.name)
let profileIdColumn: SQL = SQL(stringLiteral: Profile.Columns.id.name)
let nicknameColumn: SQL = SQL(stringLiteral: Profile.Columns.nickname.name)
let nameColumn: SQL = SQL(stringLiteral: Profile.Columns.name.name)
let quoteBodyColumn: SQL = SQL(stringLiteral: Quote.Columns.body.name)
let quoteAttachmentIdColumn: SQL = SQL(stringLiteral: Quote.Columns.attachmentId.name)
let readReceiptInteractionIdColumn: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name)
let readTimestampMsColumn: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name)
let timestampMsColumn: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
let authorIdColumn: SQL = SQL(stringLiteral: Interaction.Columns.authorId.name)
let attachmentIdColumn: SQL = SQL(stringLiteral: Attachment.Columns.id.name)
let interactionAttachmentInteractionIdColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name)
let interactionAttachmentAttachmentIdColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name)
let interactionAttachmentAlbumIndexColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name)
let numColumnsBeforeLinkedRecords: Int = 20
let numColumnsBeforeLinkedRecords: Int = 21
let finalGroupSQL: SQL = (groupSQL ?? "")
let request: SQLRequest<ViewModel> = """
SELECT
@ -674,12 +688,13 @@ public extension MessageViewModel {
IFNULL(\(disappearingMessagesConfig[.isEnabled]), false) AS \(ViewModel.threadHasDisappearingMessagesEnabledKey),
\(openGroup[.server]) AS \(ViewModel.threadOpenGroupServerKey),
\(openGroup[.publicKey]) AS \(ViewModel.threadOpenGroupPublicKeyKey),
IFNULL(\(threadProfileTableLiteral).\(profileNicknameColumnLiteral), \(threadProfileTableLiteral).\(profileNameColumnLiteral)) AS \(ViewModel.threadContactNameInternalKey),
IFNULL(\(threadProfile).\(nicknameColumn), \(threadProfile).\(nameColumn)) AS \(ViewModel.threadContactNameInternalKey),
\(interaction.alias[Column.rowID]) AS \(ViewModel.rowIdKey),
\(interaction[.id]),
\(interaction[.variant]),
\(interaction[.timestampMs]),
\(interaction[.receivedAtTimestampMs]),
\(interaction[.authorId]),
IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey),
\(interaction[.body]),
@ -688,20 +703,30 @@ public extension MessageViewModel {
-- Default to 'sending' assuming non-processed interaction when null
IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.stateKey),
(\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL) AS \(ViewModel.hasAtLeastOneReadReceiptKey),
(\(readReceipt).\(readTimestampMsColumn) IS NOT NULL) AS \(ViewModel.hasAtLeastOneReadReceiptKey),
\(recipientState[.mostRecentFailureText]) AS \(ViewModel.mostRecentFailureTextKey),
(
\(groupMemberModeratorTableLiteral).\(groupMemberProfileIdColumnLiteral) IS NOT NULL OR
\(groupMemberAdminTableLiteral).\(groupMemberProfileIdColumnLiteral) IS NOT NULL
EXISTS (
SELECT 1
FROM \(GroupMember.self)
WHERE (
\(groupMember[.groupId]) = \(interaction[.threadId]) AND
\(groupMember[.profileId]) = \(interaction[.authorId]) AND
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.openGroup)")) AND
\(SQL("\(groupMember[.role]) IN \([GroupMember.Role.moderator, GroupMember.Role.admin])"))
)
) AS \(ViewModel.isSenderOpenGroupModeratorKey),
\(ViewModel.profileKey).*,
\(ViewModel.quoteKey).*,
\(quote[.interactionId]),
\(quote[.authorId]),
\(quote[.timestampMs]),
\(quoteInteraction).\(interactionBodyColumn) AS \(quoteBodyColumn),
\(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) AS \(quoteAttachmentIdColumn),
\(ViewModel.quoteAttachmentKey).*,
\(ViewModel.linkPreviewKey).*,
\(ViewModel.linkPreviewAttachmentKey).*,
\(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey),
-- All of the below properties are set in post-query processing but to prevent the
@ -718,54 +743,40 @@ public extension MessageViewModel {
FROM \(Interaction.self)
JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId])
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(interaction[.threadId])
LEFT JOIN \(Profile.self) AS \(threadProfileTableLiteral) ON \(threadProfileTableLiteral).\(profileIdColumnLiteral) = \(interaction[.threadId])
LEFT JOIN \(Profile.self) AS \(threadProfile) ON \(threadProfile).\(profileIdColumn) = \(interaction[.threadId])
LEFT JOIN \(DisappearingMessagesConfiguration.self) ON \(disappearingMessagesConfig[.threadId]) = \(interaction[.threadId])
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId])
LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId])
LEFT JOIN (
SELECT \(quote[.interactionId]),
\(quote[.authorId]),
\(quote[.timestampMs]),
\(interaction[.body]) AS \(Quote.Columns.body),
\(interactionAttachment[.attachmentId]) AS \(Quote.Columns.attachmentId)
FROM \(Quote.self)
LEFT JOIN \(Interaction.self) ON (
(
\(quote[.authorId]) = \(interaction[.authorId]) OR (
\(quote[.authorId]) = \(blindedPublicKey ?? "") AND
\(userPublicKey) = \(interaction[.authorId])
)
) AND
\(quote[.timestampMs]) = \(interaction[.timestampMs])
LEFT JOIN \(Quote.self) ON \(quote[.interactionId]) = \(interaction[.id])
LEFT JOIN \(Interaction.self) AS \(quoteInteraction) ON (
\(quoteInteraction).\(timestampMsColumn) = \(quote[.timestampMs]) AND (
\(quoteInteraction).\(authorIdColumn) = \(quote[.authorId]) OR (
-- A users outgoing message is stored in some cases using their standard id
-- but the quote will use their blinded id so handle that case
\(quote[.authorId]) = \(blindedPublicKey ?? "''") AND
\(quoteInteraction).\(authorIdColumn) = \(userPublicKey)
)
)
LEFT JOIN \(InteractionAttachment.self) ON \(interaction[.id]) = \(interactionAttachment[.interactionId])
) AS \(ViewModel.quoteKey) ON \(quote[.interactionId]) = \(interaction[.id])
LEFT JOIN \(Attachment.self) AS \(ViewModel.quoteAttachmentKey) ON \(ViewModel.quoteAttachmentKey).\(attachmentIdColumnLiteral) = \(quote[.attachmentId])
)
LEFT JOIN \(InteractionAttachment.self) AS \(quoteInteractionAttachment) ON (
\(quoteInteractionAttachment).\(interactionAttachmentInteractionIdColumn) = \(quoteInteraction).\(idColumn) AND
\(quoteInteractionAttachment).\(interactionAttachmentAlbumIndexColumn) = 0
)
LEFT JOIN \(Attachment.self) AS \(ViewModel.quoteAttachmentKey) ON \(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn)
LEFT JOIN \(LinkPreview.self) ON (
\(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND
\(Interaction.linkPreviewFilterLiteral())
\(Interaction.linkPreviewFilterLiteral)
)
LEFT JOIN \(Attachment.self) AS \(ViewModel.linkPreviewAttachmentKey) ON \(ViewModel.linkPreviewAttachmentKey).\(attachmentIdColumnLiteral) = \(linkPreview[.attachmentId])
LEFT JOIN \(Attachment.self) AS \(ViewModel.linkPreviewAttachmentKey) ON \(ViewModel.linkPreviewAttachmentKey).\(attachmentIdColumn) = \(linkPreview[.attachmentId])
LEFT JOIN \(RecipientState.self) ON (
-- Ignore 'skipped' states
\(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND
\(recipientState[.interactionId]) = \(interaction[.id])
)
LEFT JOIN \(RecipientState.self) AS \(readReceiptTableLiteral) ON (
\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL AND
\(interaction[.id]) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral)
)
LEFT JOIN \(GroupMember.self) AS \(groupMemberModeratorTableLiteral) ON (
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND
\(groupMemberModeratorTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(interaction[.threadId]) AND
\(groupMemberModeratorTableLiteral).\(groupMemberProfileIdColumnLiteral) = \(interaction[.authorId]) AND
\(SQL("\(groupMemberModeratorTableLiteral).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.moderator)"))
)
LEFT JOIN \(GroupMember.self) AS \(groupMemberAdminTableLiteral) ON (
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND
\(groupMemberAdminTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(interaction[.threadId]) AND
\(groupMemberAdminTableLiteral).\(groupMemberProfileIdColumnLiteral) = \(interaction[.authorId]) AND
\(SQL("\(groupMemberAdminTableLiteral).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.admin)"))
LEFT JOIN \(RecipientState.self) AS \(readReceipt) ON (
\(readReceipt).\(readTimestampMsColumn) IS NOT NULL AND
\(readReceipt).\(readReceiptInteractionIdColumn) = \(interaction[.id])
)
WHERE \(interaction.alias[Column.rowID]) IN \(rowIds)
\(finalGroupSQL)

View File

@ -554,7 +554,8 @@ public extension SessionThreadViewModel {
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
let profile: TypedTableAlias<Profile> = TypedTableAlias()
let interactionTimestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction")
let timestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
let interactionStateInteractionIdColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name)
let readReceiptTableLiteral: SQL = SQL(stringLiteral: "readReceipt")
let readReceiptReadTimestampMsColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name)
@ -565,9 +566,7 @@ public extension SessionThreadViewModel {
let interactionAttachmentAttachmentIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name)
let interactionAttachmentInteractionIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name)
let interactionAttachmentAlbumIndexColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name)
let groupMemberProfileIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.profileId.name)
let groupMemberRoleColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.role.name)
let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name)
/// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before
/// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to
@ -576,14 +575,13 @@ public extension SessionThreadViewModel {
/// Explicitly set default values for the fields ignored for search results
let numColumnsBeforeProfiles: Int = 14
let numColumnsBetweenProfilesAndAttachmentInfo: Int = 12 // The attachment info columns will be combined
let request: SQLRequest<ViewModel> = """
SELECT
\(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey),
\(thread[.id]) AS \(ViewModel.threadIdKey),
\(thread[.variant]) AS \(ViewModel.threadVariantKey),
\(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey),
(\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey),
IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey),
\(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey),
@ -597,109 +595,122 @@ public extension SessionThreadViewModel {
(\(typingIndicator[.threadId]) IS NOT NULL) AS \(ViewModel.threadContactIsTypingKey),
\(thread[.markedAsUnread]) AS \(ViewModel.threadWasMarkedUnreadKey),
\(Interaction.self).\(ViewModel.threadUnreadCountKey),
\(Interaction.self).\(ViewModel.threadUnreadMentionCountKey),
\(aggregateInteractionLiteral).\(ViewModel.threadUnreadCountKey),
\(aggregateInteractionLiteral).\(ViewModel.threadUnreadMentionCountKey),
\(ViewModel.contactProfileKey).*,
\(ViewModel.closedGroupProfileFrontKey).*,
\(ViewModel.closedGroupProfileBackKey).*,
\(ViewModel.closedGroupProfileBackFallbackKey).*,
\(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey),
(\(ViewModel.currentUserIsClosedGroupMemberKey).profileId IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupMemberKey),
(\(ViewModel.currentUserIsClosedGroupAdminKey).profileId IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupAdminKey),
EXISTS (
SELECT 1
FROM \(GroupMember.self)
WHERE (
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
\(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND
\(SQL("\(groupMember[.profileId]) = \(userPublicKey)"))
)
) AS \(ViewModel.currentUserIsClosedGroupMemberKey),
EXISTS (
SELECT 1
FROM \(GroupMember.self)
WHERE (
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.admin)")) AND
\(SQL("\(groupMember[.profileId]) = \(userPublicKey)"))
)
) AS \(ViewModel.currentUserIsClosedGroupAdminKey),
\(openGroup[.name]) AS \(ViewModel.openGroupNameKey),
\(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey),
\(Interaction.self).\(ViewModel.interactionIdKey),
\(Interaction.self).\(ViewModel.interactionVariantKey),
\(Interaction.self).\(interactionTimestampMsColumnLiteral) AS \(ViewModel.interactionTimestampMsKey),
\(Interaction.self).\(ViewModel.interactionBodyKey),
\(interaction[.id]) AS \(ViewModel.interactionIdKey),
\(interaction[.variant]) AS \(ViewModel.interactionVariantKey),
\(interaction[.timestampMs]) AS \(ViewModel.interactionTimestampMsKey),
\(interaction[.body]) AS \(ViewModel.interactionBodyKey),
-- Default to 'sending' assuming non-processed interaction when null
IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.interactionStateKey),
IFNULL((
SELECT \(recipientState[.state])
FROM \(RecipientState.self)
WHERE (
\(recipientState[.interactionId]) = \(interaction[.id]) AND
-- Ignore 'skipped' states
\(SQL("\(recipientState[.state]) = \(RecipientState.State.sending)"))
)
LIMIT 1
), 0) AS \(ViewModel.interactionStateKey),
(\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL) AS \(ViewModel.interactionHasAtLeastOneReadReceiptKey),
(\(linkPreview[.url]) IS NOT NULL) AS \(ViewModel.interactionIsOpenGroupInvitationKey),
-- These 4 properties will be combined into 'Attachment.DescriptionInfo'
\(attachment[.id]),
\(attachment[.variant]),
\(attachment[.contentType]),
\(attachment[.sourceFilename]),
COUNT(\(interactionAttachment[.interactionId])) AS \(ViewModel.interactionAttachmentCountKey),
\(interaction[.authorId]),
IFNULL(\(ViewModel.contactProfileKey).\(profileNicknameColumnLiteral), \(ViewModel.contactProfileKey).\(profileNameColumnLiteral)) AS \(ViewModel.threadContactNameInternalKey),
IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey),
\(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey)
FROM \(SessionThread.self)
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
LEFT JOIN \(ThreadTypingIndicator.self) ON \(typingIndicator[.threadId]) = \(thread[.id])
LEFT JOIN (
-- Fetch all interaction-specific data in a subquery to be more efficient
SELECT
\(interaction[.id]) AS \(ViewModel.interactionIdKey),
\(interaction[.threadId]),
\(interaction[.variant]) AS \(ViewModel.interactionVariantKey),
MAX(\(interaction[.timestampMs])) AS \(interactionTimestampMsColumnLiteral),
\(interaction[.body]) AS \(ViewModel.interactionBodyKey),
\(interaction[.authorId]),
\(interaction[.linkPreviewUrl]),
\(interaction[.threadId]) AS \(ViewModel.threadIdKey),
MAX(\(interaction[.timestampMs])) AS \(timestampMsColumnLiteral),
SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey),
SUM(\(interaction[.wasRead]) = false AND \(interaction[.hasMention]) = true) AS \(ViewModel.threadUnreadMentionCountKey)
FROM \(Interaction.self)
WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)"))
GROUP BY \(interaction[.threadId])
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])
) AS \(aggregateInteractionLiteral) ON \(aggregateInteractionLiteral).\(ViewModel.threadIdKey) = \(thread[.id])
LEFT JOIN \(RecipientState.self) ON (
-- Ignore 'skipped' states
\(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND
\(recipientState[.interactionId]) = \(Interaction.self).\(ViewModel.interactionIdKey)
LEFT JOIN \(Interaction.self) ON (
\(interaction[.threadId]) = \(thread[.id]) AND
\(interaction[.id]) = \(aggregateInteractionLiteral).\(ViewModel.interactionIdKey)
)
LEFT JOIN \(RecipientState.self) AS \(readReceiptTableLiteral) ON (
\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL AND
\(Interaction.self).\(ViewModel.interactionIdKey) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral)
\(interaction[.id]) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral) AND
\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL
)
LEFT JOIN \(LinkPreview.self) ON (
\(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND
\(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)")) AND
\(Interaction.linkPreviewFilterLiteral(timestampColumn: interactionTimestampMsColumnLiteral))
\(Interaction.linkPreviewFilterLiteral) AND
\(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)"))
)
LEFT JOIN \(InteractionAttachment.self) AS \(firstInteractionAttachmentLiteral) ON (
\(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0 AND
\(firstInteractionAttachmentLiteral).\(interactionAttachmentInteractionIdColumnLiteral) = \(Interaction.self).\(ViewModel.interactionIdKey)
\(firstInteractionAttachmentLiteral).\(interactionAttachmentInteractionIdColumnLiteral) = \(interaction[.id]) AND
\(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0
)
LEFT JOIN \(Attachment.self) ON \(attachment[.id]) = \(firstInteractionAttachmentLiteral).\(interactionAttachmentAttachmentIdColumnLiteral)
LEFT JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.interactionId]) = \(Interaction.self).\(ViewModel.interactionIdKey)
LEFT JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.interactionId]) = \(interaction[.id])
LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId])
-- Thread naming & avatar content
LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id])
LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id])
LEFT JOIN \(GroupMember.self) AS \(ViewModel.currentUserIsClosedGroupMemberKey) ON (
\(SQL("\(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberRoleColumnLiteral) != \(GroupMember.Role.zombie)")) AND
\(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId]) AND
\(SQL("\(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberProfileIdColumnLiteral) = \(userPublicKey)"))
)
LEFT JOIN \(GroupMember.self) AS \(ViewModel.currentUserIsClosedGroupAdminKey) ON (
\(SQL("\(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.admin)")) AND
\(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId]) AND
\(SQL("\(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberProfileIdColumnLiteral) = \(userPublicKey)"))
)
LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON (
\(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = (
SELECT MIN(\(groupMember[.profileId]))
FROM \(GroupMember.self)
JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId])
WHERE (
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
\(SQL("\(groupMember[.profileId]) != \(userPublicKey)"))
)
)
@ -711,8 +722,8 @@ public extension SessionThreadViewModel {
FROM \(GroupMember.self)
JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId])
WHERE (
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
\(SQL("\(groupMember[.profileId]) != \(userPublicKey)"))
)
)
@ -722,7 +733,7 @@ public extension SessionThreadViewModel {
\(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND
\(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(SQL("\(userPublicKey)"))
)
WHERE \(thread.alias[Column.rowID]) IN \(rowIds)
\(groupSQL)
ORDER BY \(orderSQL)
@ -755,14 +766,14 @@ public extension SessionThreadViewModel {
let contact: TypedTableAlias<Contact> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let interactionTimestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
let timestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
return """
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
LEFT JOIN (
SELECT
\(interaction[.threadId]),
MAX(\(interaction[.timestampMs])) AS \(interactionTimestampMsColumnLiteral)
MAX(\(interaction[.timestampMs])) AS \(timestampMsColumnLiteral)
FROM \(Interaction.self)
WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)"))
GROUP BY \(interaction[.threadId])
@ -813,7 +824,10 @@ public extension SessionThreadViewModel {
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
return SQL("(IFNULL(\(thread[.pinnedPriority]), 0) > 0) DESC, IFNULL(\(interaction[.timestampMs]), (\(thread[.creationDateTimestamp]) * 1000)) DESC")
return SQL("""
(IFNULL(\(thread[.pinnedPriority]), 0) > 0) DESC,
IFNULL(\(interaction[.timestampMs]), (\(thread[.creationDateTimestamp]) * 1000)) END DESC
""")
}()
static let messageRequetsOrderSQL: SQL = {
@ -837,6 +851,8 @@ public extension SessionThreadViewModel {
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction")
let timestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name)
let closedGroupUserCountTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.closedGroupUserCountString)_table")
let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name)
let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name)
@ -871,14 +887,24 @@ public extension SessionThreadViewModel {
\(thread[.mutedUntilTimestamp]) AS \(ViewModel.threadMutedUntilTimestampKey),
\(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey),
\(thread[.messageDraft]) AS \(ViewModel.threadMessageDraftKey),
\(thread[.markedAsUnread]) AS \(ViewModel.threadWasMarkedUnreadKey),
\(Interaction.self).\(ViewModel.threadUnreadCountKey),
\(aggregateInteractionLiteral).\(ViewModel.threadUnreadCountKey),
\(ViewModel.contactProfileKey).*,
\(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey),
\(closedGroupUserCountTableLiteral).\(ViewModel.closedGroupUserCountKey) AS \(ViewModel.closedGroupUserCountKey),
(\(groupMember[.profileId]) IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupMemberKey),
EXISTS (
SELECT 1
FROM \(GroupMember.self)
WHERE (
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
\(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND
\(SQL("\(groupMember[.profileId]) = \(userPublicKey)"))
)
) AS \(ViewModel.currentUserIsClosedGroupMemberKey),
\(openGroup[.name]) AS \(ViewModel.openGroupNameKey),
\(openGroup[.server]) AS \(ViewModel.openGroupServerKey),
\(openGroup[.roomToken]) AS \(ViewModel.openGroupRoomTokenKey),
@ -886,34 +912,29 @@ public extension SessionThreadViewModel {
\(openGroup[.userCount]) AS \(ViewModel.openGroupUserCountKey),
\(openGroup[.permissions]) AS \(ViewModel.openGroupPermissionsKey),
\(Interaction.self).\(ViewModel.interactionIdKey),
\(Interaction.self).\(ViewModel.interactionTimestampMsKey),
\(aggregateInteractionLiteral).\(ViewModel.interactionIdKey),
\(aggregateInteractionLiteral).\(ViewModel.interactionTimestampMsKey),
\(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey)
FROM \(SessionThread.self)
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
LEFT JOIN (
-- Fetch all interaction-specific data in a subquery to be more efficient
SELECT
\(interaction[.id]) AS \(ViewModel.interactionIdKey),
\(interaction[.threadId]),
\(interaction[.threadId]) AS \(ViewModel.threadIdKey),
MAX(\(interaction[.timestampMs])) AS \(ViewModel.interactionTimestampMsKey),
SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey)
FROM \(Interaction.self)
WHERE \(SQL("\(interaction[.threadId]) = \(threadId)"))
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])
WHERE (
\(SQL("\(interaction[.threadId]) = \(threadId)")) AND
\(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)"))
)
) AS \(aggregateInteractionLiteral) ON \(aggregateInteractionLiteral).\(ViewModel.threadIdKey) = \(thread[.id])
LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id])
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id])
LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id])
LEFT JOIN \(GroupMember.self) ON (
\(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) AND
\(groupMember[.groupId]) = \(closedGroup[.threadId]) AND
\(SQL("\(groupMember[.profileId]) = \(userPublicKey)"))
)
LEFT JOIN (
SELECT
\(groupMember[.groupId]),
@ -1700,7 +1721,7 @@ public extension SessionThreadViewModel {
FROM \(SessionThread.self)
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
LEFT JOIN (
SELECT *, MAX(\(interaction[.timestampMs]))
SELECT \(interaction[.threadId]), MAX(\(interaction[.timestampMs]))
FROM \(Interaction.self)
GROUP BY \(interaction[.threadId])
) AS \(Interaction.self) ON \(interaction[.threadId]) = \(thread[.id])

View File

@ -308,9 +308,16 @@ public enum Preferences {
}
public static var isCallKitSupported: Bool {
#if targetEnvironment(simulator)
/// The iOS simulator doesn't support CallKit, when receiving a call on the simulator and routing it via CallKit it
/// will immediately trigger a hangup making it difficult to test - instead we just should just avoid using CallKit
/// entirely on the simulator
return false
#else
guard let regionCode: String = NSLocale.current.regionCode else { return false }
guard !regionCode.contains("CN") && !regionCode.contains("CHN") else { return false }
return true
#endif
}
}

View File

@ -514,16 +514,13 @@ public struct ProfileManager {
// Name
if let name: String = name, !name.isEmpty, name != profile.name {
let shouldUpdate: Bool
if isCurrentUser {
shouldUpdate = given(UserDefaults.standard[.lastDisplayNameUpdate]) {
sentTimestamp > $0.timeIntervalSince1970
}
.defaulting(to: true)
}
else {
shouldUpdate = true
}
let shouldUpdate: Bool = {
guard isCurrentUser else { return true }
return UserDefaults.standard[.lastDisplayNameUpdate]
.map { sentTimestamp > $0.timeIntervalSince1970 }
.defaulting(to: true)
}()
if shouldUpdate {
if isCurrentUser {
@ -540,10 +537,9 @@ public struct ProfileManager {
let shouldUpdateAvatar: Bool = {
guard isCurrentUser else { return true }
return given(UserDefaults.standard[.lastProfilePictureUpdate]) {
sentTimestamp > $0.timeIntervalSince1970
}
.defaulting(to: true)
return UserDefaults.standard[.lastProfilePictureUpdate]
.map { sentTimestamp > $0.timeIntervalSince1970 }
.defaulting(to: true)
}()
if shouldUpdateAvatar {

View File

@ -41,9 +41,9 @@ public final class SnodeAPI {
}
return [
"https://storage.seed1.loki.network:4433",
"https://storage.seed3.loki.network:4433",
"https://public.loki.foundation:4433"
"https://seed1.getsession.org:4432",
"https://seed2.getsession.org:4432",
"https://seed3.getsession.org:4432"
]
}()
private static let snodeFailureThreshold: Int = 3
@ -139,9 +139,9 @@ public final class SnodeAPI {
loadSnodePoolIfNeeded()
let now: Date = Date()
let hasSnodePoolExpired: Bool = given(Storage.shared[.lastSnodePoolRefreshDate]) {
now.timeIntervalSince($0) > 2 * 60 * 60
}.defaulting(to: true)
let hasSnodePoolExpired: Bool = Storage.shared[.lastSnodePoolRefreshDate]
.map { now.timeIntervalSince($0) > 2 * 60 * 60 }
.defaulting(to: true)
let snodePool: Set<Snode> = SnodeAPI.snodePool.wrappedValue
guard hasInsufficientSnodes || hasSnodePoolExpired else {
@ -154,35 +154,42 @@ public final class SnodeAPI {
return getSnodePoolPublisher
}
let publisher: AnyPublisher<Set<Snode>, Error>
if snodePool.count < minSnodePoolCount {
publisher = getSnodePoolFromSeedNode()
}
else {
publisher = getSnodePoolFromSnode()
.catch { _ in getSnodePoolFromSeedNode() }
return getSnodePoolPublisher.mutate { result in
/// It was possible for multiple threads to call this at the same time resulting in duplicate promises getting created, while
/// this should no longer be possible (as the `wrappedValue` should now properly be blocked) this is a sanity check
/// to make sure we don't create an additional promise when one already exists
if let previouslyBlockedPublisher: AnyPublisher<Set<Snode>, Error> = result {
return previouslyBlockedPublisher
}
let publisher: AnyPublisher<Set<Snode>, Error> = {
guard snodePool.count >= minSnodePoolCount else { return getSnodePoolFromSeedNode() }
return getSnodePoolFromSnode()
.catch { _ in getSnodePoolFromSeedNode() }
.eraseToAnyPublisher()
}()
getSnodePoolPublisher.mutate { $0 = publisher }
return publisher
.tryFlatMap { snodePool -> AnyPublisher<Set<Snode>, Error> in
guard !snodePool.isEmpty else { throw SnodeAPIError.snodePoolUpdatingFailed }
return Storage.shared
.writePublisher(receiveOn: Threading.workQueue) { db in
db[.lastSnodePoolRefreshDate] = now
setSnodePool(db, to: snodePool)
return snodePool
}
.eraseToAnyPublisher()
}
.handleEvents(
receiveCompletion: { _ in getSnodePoolPublisher.mutate { $0 = nil } }
)
.eraseToAnyPublisher()
}
getSnodePoolPublisher.mutate { $0 = publisher }
return publisher
.tryFlatMap { snodePool -> AnyPublisher<Set<Snode>, Error> in
guard !snodePool.isEmpty else { throw SnodeAPIError.snodePoolUpdatingFailed }
return Storage.shared
.writePublisher(receiveOn: Threading.workQueue) { db in
db[.lastSnodePoolRefreshDate] = now
setSnodePool(db, to: snodePool)
return snodePool
}
.eraseToAnyPublisher()
}
.handleEvents(
receiveCompletion: { _ in getSnodePoolPublisher.mutate { $0 = nil } }
)
.eraseToAnyPublisher()
}
public static func getSessionID(for onsName: String) -> AnyPublisher<String, Error> {

View File

@ -17,13 +17,13 @@ public extension UIView {
class func spacer(withWidth width: CGFloat) -> UIView {
let view = UIView()
view.autoSetDimension(.width, toSize: width)
view.set(.width, to: width)
return view
}
class func spacer(withHeight height: CGFloat) -> UIView {
let view = UIView()
view.autoSetDimension(.height, toSize: height)
view.set(.height, to: height)
return view
}

View File

@ -35,14 +35,3 @@ public func getUserHexEncodedPublicKey(_ db: Database? = nil, dependencies: Depe
return ""
}
/// Does nothing, but is never inlined and thus evaluating its argument will never be optimized away.
///
/// Useful for forcing the instantiation of lazy properties like globals.
@inline(never)
public func touch<Value>(_ value: Value) { /* Do nothing */ }
/// Returns `f(x!)` if `x != nil`, or `nil` otherwise.
public func given<T, U>(_ x: T?, _ f: (T) throws -> U) rethrows -> U? { return try x.map(f) }
public func with<T, U>(_ x: T, _ f: (T) throws -> U) rethrows -> U { return try f(x) }

View File

@ -85,6 +85,15 @@ public extension String {
let secondsPerWeek: TimeInterval = (secondsPerDay * 7)
switch format {
case .videoDuration:
let seconds: Int = Int(duration.truncatingRemainder(dividingBy: 60))
let minutes: Int = Int((duration / 60).truncatingRemainder(dividingBy: 60))
let hours: Int = Int(duration / 3600)
guard hours > 0 else { return String(format: "%02ld:%02ld", minutes, seconds) }
return String(format: "%ld:%02ld:%02ld", hours, minutes, seconds)
case .hoursMinutesSeconds:
let seconds: Int = Int(duration.truncatingRemainder(dividingBy: 60))
let minutes: Int = Int((duration / 60).truncatingRemainder(dividingBy: 60))

View File

@ -7,6 +7,7 @@ public extension TimeInterval {
case short
case long
case hoursMinutesSeconds
case videoDuration
}
func formatted(format: DurationFormat) -> String {

View File

@ -2,7 +2,6 @@
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import <PureLayout/PureLayout.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN

View File

@ -5,6 +5,7 @@
#import "UIView+OWS.h"
#import "OWSMath.h"
#import <PureLayout/PureLayout.h>
#import <SessionUtilitiesKit/AppContext.h>
NS_ASSUME_NONNULL_BEGIN

View File

@ -600,12 +600,17 @@ private final class JobQueue {
}
fileprivate func appDidBecomeActive(with jobs: [Job], canStart: Bool) {
let currentlyRunningJobIds: Set<Int64> = jobsCurrentlyRunning.wrappedValue
queue.mutate { queue in
// Avoid re-adding jobs to the queue that are already in it (this can
// happen if the user sends the app to the background before the 'onActive'
// jobs and then brings it back to the foreground)
let jobsNotAlreadyInQueue: [Job] = jobs
.filter { job in !queue.contains(where: { $0.id == job.id }) }
.filter { job in
!currentlyRunningJobIds.contains(job.id ?? -1) &&
!queue.contains(where: { $0.id == job.id })
}
queue.append(contentsOf: jobsNotAlreadyInQueue)
}
@ -797,14 +802,20 @@ private final class JobQueue {
guard dependencyInfo.jobs.isEmpty else {
SNLog("[JobRunner] \(queueContext) found job with \(dependencyInfo.jobs.count) dependencies, running those first")
/// Remove all jobs this one is dependant on from the queue and re-insert them at the start of the queue
/// Remove all jobs this one is dependant on that aren't currently running from the queue and re-insert them at the start
/// of the queue
///
/// **Note:** We don't add the current job back the the queue because it should only be re-added if it's dependencies
/// are successfully completed
let currentlyRunningJobIds: [Int64] = Array(detailsForCurrentlyRunningJobs.wrappedValue.keys)
let dependencyJobsNotCurrentlyRunning: [Job] = dependencyInfo.jobs
.filter { job in !currentlyRunningJobIds.contains(job.id ?? -1) }
.sorted { lhs, rhs in (lhs.id ?? -1) < (rhs.id ?? -1) }
queue.mutate { queue in
queue = queue
.filter { !dependencyInfo.jobs.contains($0) }
.inserting(contentsOf: Array(dependencyInfo.jobs), at: 0)
.filter { !dependencyJobsNotCurrentlyRunning.contains($0) }
.inserting(contentsOf: dependencyJobsNotCurrentlyRunning, at: 0)
}
handleJobDeferred(nextJob)
return
@ -991,17 +1002,22 @@ private final class JobQueue {
default: break
}
/// Now that the job has been completed we want to insert any jobs that were dependant on it to the start of the queue (the
/// most likely case is that we want an entire job chain to be completed at the same time rather than being blocked by other
/// unrelated jobs)
/// Now that the job has been completed we want to insert any jobs that were dependant on it, that aren't already running
/// to the start of the queue (the most likely case is that we want an entire job chain to be completed at the same time rather
/// than being blocked by other unrelated jobs)
///
/// **Note:** If any of these `dependantJobs` have other dependencies then when they attempt to start they will be
/// removed from the queue, replaced by their dependencies
if !dependantJobs.isEmpty {
let currentlyRunningJobIds: [Int64] = Array(detailsForCurrentlyRunningJobs.wrappedValue.keys)
let dependantJobsNotCurrentlyRunning: [Job] = dependantJobs
.filter { job in !currentlyRunningJobIds.contains(job.id ?? -1) }
.sorted { lhs, rhs in (lhs.id ?? -1) < (rhs.id ?? -1) }
queue.mutate { queue in
queue = queue
.filter { !dependantJobs.contains($0) }
.inserting(contentsOf: dependantJobs, at: 0)
.filter { !dependantJobsNotCurrentlyRunning.contains($0) }
.inserting(contentsOf: dependantJobsNotCurrentlyRunning, at: 0)
}
}

View File

@ -11,20 +11,23 @@ public enum HTTP {
// MARK: - Certificates
/// **Note:** These certificates will need to be regenerated and replaced at the start of April 2025, iOS has a restriction after iOS 13
/// where certificates can have a maximum lifetime of 825 days (https://support.apple.com/en-au/HT210176) as a result we
/// can't use the 10 year certificates that the other platforms use
private static let storageSeed1Cert: SecCertificate = {
let path = Bundle.main.path(forResource: "storage-seed-1", ofType: "der")!
let path = Bundle.main.path(forResource: "seed1-2023-2y", ofType: "der")!
let data = try! Data(contentsOf: URL(fileURLWithPath: path))
return SecCertificateCreateWithData(nil, data as CFData)!
}()
private static let storageSeed2Cert: SecCertificate = {
let path = Bundle.main.path(forResource: "seed2-2023-2y", ofType: "der")!
let data = try! Data(contentsOf: URL(fileURLWithPath: path))
return SecCertificateCreateWithData(nil, data as CFData)!
}()
private static let storageSeed3Cert: SecCertificate = {
let path = Bundle.main.path(forResource: "storage-seed-3", ofType: "der")!
let data = try! Data(contentsOf: URL(fileURLWithPath: path))
return SecCertificateCreateWithData(nil, data as CFData)!
}()
private static let publicLokiFoundationCert: SecCertificate = {
let path = Bundle.main.path(forResource: "public-loki-foundation", ofType: "der")!
let path = Bundle.main.path(forResource: "seed3-2023-2y", ofType: "der")!
let data = try! Data(contentsOf: URL(fileURLWithPath: path))
return SecCertificateCreateWithData(nil, data as CFData)!
}()
@ -42,41 +45,51 @@ public enum HTTP {
return completionHandler(.cancelAuthenticationChallenge, nil)
}
// Mark the seed node certificates as trusted
let certificates = [ storageSeed1Cert, storageSeed3Cert, publicLokiFoundationCert ]
let certificates = [ storageSeed1Cert, storageSeed2Cert, storageSeed3Cert ]
guard SecTrustSetAnchorCertificates(trust, certificates as CFArray) == errSecSuccess else {
SNLog("Failed to set seed node certificates.")
return completionHandler(.cancelAuthenticationChallenge, nil)
}
// We want to make sure that the pinned certification was valid during it's validity
// period (which has now expired) so set the date to validate against to be within the
// valid period
let dateFormatter: DateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd/MM/yyyy HH:mm:ss"
if let validDate: Date = dateFormatter.date(from: "01/01/2022 12:00:00") {
if SecTrustSetVerifyDate(trust, validDate as CFDate) != errSecSuccess {
SNLog("Unable to set date for seed node certificate validation.")
}
}
else {
SNLog("Unable to set date for seed node certificate validation.")
}
// Check that the presented certificate is one of the seed node certificates
var result: SecTrustResultType = .invalid
var error: CFError?
guard SecTrustEvaluateWithError(trust, &error) else {
// Extract the result for further processing (since we are defaulting to `invalid` we
// don't care if extracting the result type fails)
var result: SecTrustResultType = .invalid
_ = SecTrustGetTrustResult(trust, &result)
switch result {
case .proceed, .unspecified:
/// Unspecified indicates that evaluation reached an (implicitly trusted) anchor certificate without any evaluation
/// failures, but never encountered any explicitly stated user-trust preference. This is the most common return
/// value. The Keychain Access utility refers to this value as the "Use System Policy," which is the default user setting.
return completionHandler(.useCredential, URLCredential(trust: trust))
case .recoverableTrustFailure:
/// A recoverable failure generally suggests that the certificate was mostly valid but something minor didn't line up,
/// while we don't want to recover in this case it's probably a good idea to include the reason in the logs to simplify
/// debugging if it does end up happening
let reason: String = {
guard
let validationResult: [String: Any] = SecTrustCopyResult(trust) as? [String: Any],
let details: [String: Any] = (validationResult["TrustResultDetails"] as? [[String: Any]])?
.reduce(into: [:], { result, next in next.forEach { result[$0.key] = $0.value } })
else { return "Unknown" }
return "\(details)"
}()
SNLog("Failed to validate a seed certificate with a recoverable error: \(reason)")
return completionHandler(.cancelAuthenticationChallenge, nil)
default:
SNLog("Failed to validate a seed certificate with an unrecoverable error.")
return completionHandler(.cancelAuthenticationChallenge, nil)
}
}
guard SecTrustEvaluate(trust, &result) == errSecSuccess else {
return completionHandler(.cancelAuthenticationChallenge, nil)
}
switch result {
case .proceed, .unspecified:
// Unspecified indicates that evaluation reached an (implicitly trusted) anchor certificate without
// any evaluation failures, but never encountered any explicitly stated user-trust preference. This
// is the most common return value. The Keychain Access utility refers to this value as the "Use System
// Policy," which is the default user setting.
return completionHandler(.useCredential, URLCredential(trust: trust))
default: return completionHandler(.cancelAuthenticationChallenge, nil)
}
return completionHandler(.useCredential, URLCredential(trust: trust))
}
}

View File

@ -4,6 +4,7 @@ import Foundation
import UIKit
import SessionUIKit
import SignalCoreKit
import PureLayout
// Coincides with Android's max text message length
let kMaxMessageBodyCharacterCount = 2000