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:
commit
fa39b5f61c
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -30,7 +30,7 @@ public class MediaAlbumView: UIStackView {
|
|||
mediaCache: mediaCache,
|
||||
attachment: $0,
|
||||
isOutgoing: isOutgoing,
|
||||
maxMessageWidth: maxMessageWidth
|
||||
cornerRadius: VisibleMessageCell.largeCornerRadius
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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])
|
||||
}
|
||||
}
|
|
@ -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-----
|
Binary file not shown.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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-----
|
Binary file not shown.
|
@ -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-----
|
Binary file not shown.
|
@ -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/>
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol SessionCarouselViewDelegate: AnyObject {
|
||||
func carouselViewDidScrollToNewSlice(currentPage: Int)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -7,6 +7,7 @@ public extension TimeInterval {
|
|||
case short
|
||||
case long
|
||||
case hoursMinutesSeconds
|
||||
case videoDuration
|
||||
}
|
||||
|
||||
func formatted(format: DurationFormat) -> String {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#import "UIView+OWS.h"
|
||||
#import "OWSMath.h"
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import <SessionUtilitiesKit/AppContext.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue