Did some more theming, removed some files and fixed a couple of minor call issues

Applied theming logic to the ConversationTitleView, blocked banner
Removed a few redundant modals (replaced them with the "Confirmation Modal")
Removed some duplicate code
Fixed an issue where a synchronous start/stop behaviour was running on the main thread causing some UI blocking
Fixed an issue where the minimised call view could be covered by presenting view controllers
This commit is contained in:
Morgan Pretty 2022-09-02 17:32:13 +10:00
parent b47e5accd6
commit b029728b6c
53 changed files with 973 additions and 975 deletions

View File

@ -111,7 +111,6 @@
7B0EFDF4275490EA00FFAAE7 /* ringing.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 7B0EFDF3275490EA00FFAAE7 /* ringing.mp3 */; }; 7B0EFDF4275490EA00FFAAE7 /* ringing.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 7B0EFDF3275490EA00FFAAE7 /* ringing.mp3 */; };
7B0EFDF62755CC5400FFAAE7 /* CallMissedTipsModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0EFDF52755CC5400FFAAE7 /* CallMissedTipsModal.swift */; }; 7B0EFDF62755CC5400FFAAE7 /* CallMissedTipsModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0EFDF52755CC5400FFAAE7 /* CallMissedTipsModal.swift */; };
7B13E1E92810F01300BD4F64 /* SessionCallManager+Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B13E1E82810F01300BD4F64 /* SessionCallManager+Action.swift */; }; 7B13E1E92810F01300BD4F64 /* SessionCallManager+Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B13E1E82810F01300BD4F64 /* SessionCallManager+Action.swift */; };
7B1581E4271FC59D00848B49 /* CallModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1581E3271FC59C00848B49 /* CallModal.swift */; };
7B1581E6271FD2A100848B49 /* VideoPreviewVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1581E5271FD2A100848B49 /* VideoPreviewVC.swift */; }; 7B1581E6271FD2A100848B49 /* VideoPreviewVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1581E5271FD2A100848B49 /* VideoPreviewVC.swift */; };
7B1581E827210ECC00848B49 /* RenderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1581E727210ECC00848B49 /* RenderView.swift */; }; 7B1581E827210ECC00848B49 /* RenderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1581E727210ECC00848B49 /* RenderView.swift */; };
7B1B52D828580C6D006069F2 /* EmojiPickerSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52D728580C6D006069F2 /* EmojiPickerSheet.swift */; }; 7B1B52D828580C6D006069F2 /* EmojiPickerSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52D728580C6D006069F2 /* EmojiPickerSheet.swift */; };
@ -189,8 +188,6 @@
B81D25C426157F40004D1FE1 /* storage-seed-3.crt in Resources */ = {isa = PBXBuildFile; fileRef = B81D25B926157F20004D1FE1 /* storage-seed-3.crt */; }; 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 */; }; 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 */; }; B81D25C626157F40004D1FE1 /* public-loki-foundation.crt in Resources */ = {isa = PBXBuildFile; fileRef = B81D25B826157F20004D1FE1 /* public-loki-foundation.crt */; };
B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B821494525D4D6FF009C0F2A /* URLModal.swift */; };
B82149B825D60393009C0F2A /* BlockedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82149B725D60393009C0F2A /* BlockedModal.swift */; };
B82149C125D605C6009C0F2A /* InfoBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82149C025D605C6009C0F2A /* InfoBanner.swift */; }; B82149C125D605C6009C0F2A /* InfoBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B82149C025D605C6009C0F2A /* InfoBanner.swift */; };
B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D2825C7A4B400488AB4 /* InputView.swift */; }; B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D2825C7A4B400488AB4 /* InputView.swift */; };
B8269D3325C7A8C600488AB4 /* InputViewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D3225C7A8C600488AB4 /* InputViewButton.swift */; }; B8269D3325C7A8C600488AB4 /* InputViewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D3225C7A8C600488AB4 /* InputViewButton.swift */; };
@ -272,7 +269,6 @@
B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */; }; B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */; };
B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */; }; B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */; };
B8F5F71A25F1B35C003BF8D4 /* MediaPlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F71925F1B35C003BF8D4 /* MediaPlaceholderView.swift */; }; B8F5F71A25F1B35C003BF8D4 /* MediaPlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F71925F1B35C003BF8D4 /* MediaPlaceholderView.swift */; };
B8F5F72325F1B4CA003BF8D4 /* DownloadAttachmentModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F72225F1B4CA003BF8D4 /* DownloadAttachmentModal.swift */; };
B8FF8DAE25C0D00F004D1F22 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; }; B8FF8DAE25C0D00F004D1F22 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; };
B8FF8DAF25C0D00F004D1F22 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; B8FF8DAF25C0D00F004D1F22 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
B8FF8E6225C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 in Resources */ = {isa = PBXBuildFile; fileRef = B8FF8E6125C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 */; }; B8FF8E6225C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 in Resources */ = {isa = PBXBuildFile; fileRef = B8FF8E6125C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 */; };
@ -482,7 +478,6 @@
C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D662558A0170043A11F /* DiffieHellman.swift */; }; C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D662558A0170043A11F /* DiffieHellman.swift */; };
C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */; }; C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */; };
C3A7225E2558C38D0043A11F /* Promise+Retaining.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7225D2558C38D0043A11F /* Promise+Retaining.swift */; }; C3A7225E2558C38D0043A11F /* Promise+Retaining.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7225D2558C38D0043A11F /* Promise+Retaining.swift */; };
C3A76A8D25DB83F90074CB90 /* PermissionMissingModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */; };
C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFF125AE99710089E6DD /* AppDelegate.swift */; }; C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFF125AE99710089E6DD /* AppDelegate.swift */; };
C3ADC66126426688005F1414 /* ShareVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ADC66026426688005F1414 /* ShareVC.swift */; }; C3ADC66126426688005F1414 /* ShareVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ADC66026426688005F1414 /* ShareVC.swift */; };
C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D62553860B00C340D1 /* Promise+Retrying.swift */; }; C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D62553860B00C340D1 /* Promise+Retrying.swift */; };
@ -1173,7 +1168,6 @@
7B0EFDF3275490EA00FFAAE7 /* ringing.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = ringing.mp3; sourceTree = "<group>"; }; 7B0EFDF3275490EA00FFAAE7 /* ringing.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = ringing.mp3; sourceTree = "<group>"; };
7B0EFDF52755CC5400FFAAE7 /* CallMissedTipsModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMissedTipsModal.swift; sourceTree = "<group>"; }; 7B0EFDF52755CC5400FFAAE7 /* CallMissedTipsModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMissedTipsModal.swift; sourceTree = "<group>"; };
7B13E1E82810F01300BD4F64 /* SessionCallManager+Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+Action.swift"; sourceTree = "<group>"; }; 7B13E1E82810F01300BD4F64 /* SessionCallManager+Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionCallManager+Action.swift"; sourceTree = "<group>"; };
7B1581E3271FC59C00848B49 /* CallModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallModal.swift; sourceTree = "<group>"; };
7B1581E5271FD2A100848B49 /* VideoPreviewVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPreviewVC.swift; sourceTree = "<group>"; }; 7B1581E5271FD2A100848B49 /* VideoPreviewVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPreviewVC.swift; sourceTree = "<group>"; };
7B1581E727210ECC00848B49 /* RenderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderView.swift; sourceTree = "<group>"; }; 7B1581E727210ECC00848B49 /* RenderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderView.swift; sourceTree = "<group>"; };
7B1B52D728580C6D006069F2 /* EmojiPickerSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerSheet.swift; sourceTree = "<group>"; }; 7B1B52D728580C6D006069F2 /* EmojiPickerSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerSheet.swift; sourceTree = "<group>"; };
@ -1263,8 +1257,6 @@
B81D25B726157F20004D1FE1 /* storage-seed-1.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "storage-seed-1.crt"; 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>"; }; 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>"; }; B81D25B926157F20004D1FE1 /* storage-seed-3.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "storage-seed-3.crt"; sourceTree = "<group>"; };
B821494525D4D6FF009C0F2A /* URLModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLModal.swift; sourceTree = "<group>"; };
B82149B725D60393009C0F2A /* BlockedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedModal.swift; sourceTree = "<group>"; };
B82149C025D605C6009C0F2A /* InfoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoBanner.swift; 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>"; }; 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>"; }; B8269D3225C7A8C600488AB4 /* InputViewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputViewButton.swift; sourceTree = "<group>"; };
@ -1352,7 +1344,6 @@
B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Subscripting.swift"; sourceTree = "<group>"; }; B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+Subscripting.swift"; sourceTree = "<group>"; };
B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExtractionNotification.swift; sourceTree = "<group>"; }; B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExtractionNotification.swift; sourceTree = "<group>"; };
B8F5F71925F1B35C003BF8D4 /* MediaPlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlaceholderView.swift; sourceTree = "<group>"; }; B8F5F71925F1B35C003BF8D4 /* MediaPlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlaceholderView.swift; sourceTree = "<group>"; };
B8F5F72225F1B4CA003BF8D4 /* DownloadAttachmentModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadAttachmentModal.swift; sourceTree = "<group>"; };
B8FF8E6125C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "GeoLite2-Country-Blocks-IPv4"; path = "Countries/GeoLite2-Country-Blocks-IPv4"; sourceTree = "<group>"; }; B8FF8E6125C10DA5004D1F22 /* GeoLite2-Country-Blocks-IPv4 */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "GeoLite2-Country-Blocks-IPv4"; path = "Countries/GeoLite2-Country-Blocks-IPv4"; sourceTree = "<group>"; };
B8FF8E7325C10FC3004D1F22 /* GeoLite2-Country-Locations-English */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "GeoLite2-Country-Locations-English"; path = "Countries/GeoLite2-Country-Locations-English"; sourceTree = "<group>"; }; B8FF8E7325C10FC3004D1F22 /* GeoLite2-Country-Locations-English */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = "GeoLite2-Country-Locations-English"; path = "Countries/GeoLite2-Country-Locations-English"; sourceTree = "<group>"; };
B8FF8EA525C11FEF004D1F22 /* IPv4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv4.swift; sourceTree = "<group>"; }; B8FF8EA525C11FEF004D1F22 /* IPv4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPv4.swift; sourceTree = "<group>"; };
@ -1585,7 +1576,6 @@
C3A71F882558BA9F0043A11F /* Mnemonic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mnemonic.swift; sourceTree = "<group>"; }; C3A71F882558BA9F0043A11F /* Mnemonic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mnemonic.swift; sourceTree = "<group>"; };
C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyPromise+Conversion.swift"; sourceTree = "<group>"; }; C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyPromise+Conversion.swift"; sourceTree = "<group>"; };
C3A7225D2558C38D0043A11F /* Promise+Retaining.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Retaining.swift"; sourceTree = "<group>"; }; C3A7225D2558C38D0043A11F /* Promise+Retaining.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Retaining.swift"; sourceTree = "<group>"; };
C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionMissingModal.swift; sourceTree = "<group>"; };
C3A8AF752665B03900A467FE /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = "<group>"; }; C3A8AF752665B03900A467FE /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = "<group>"; };
C3A8AF762665F97A00A467FE /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; }; C3A8AF762665F97A00A467FE /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
C3AAFFF125AE99710089E6DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; C3AAFFF125AE99710089E6DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -2359,17 +2349,12 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */, B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */,
B8F5F72225F1B4CA003BF8D4 /* DownloadAttachmentModal.swift */,
C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */,
B821494525D4D6FF009C0F2A /* URLModal.swift */,
B8AF4BB326A5204600583500 /* SendSeedModal.swift */, B8AF4BB326A5204600583500 /* SendSeedModal.swift */,
B8758859264503A6000E60D0 /* JoinOpenGroupModal.swift */, B8758859264503A6000E60D0 /* JoinOpenGroupModal.swift */,
B82149B725D60393009C0F2A /* BlockedModal.swift */,
B82149C025D605C6009C0F2A /* InfoBanner.swift */, B82149C025D605C6009C0F2A /* InfoBanner.swift */,
C374EEEA25DA3CA70073A857 /* ConversationTitleView.swift */, C374EEEA25DA3CA70073A857 /* ConversationTitleView.swift */,
B848A4C4269EAAA200617031 /* UserDetailsSheet.swift */, B848A4C4269EAAA200617031 /* UserDetailsSheet.swift */,
FD4B200D283492210034334B /* InsetLockableTableView.swift */, FD4B200D283492210034334B /* InsetLockableTableView.swift */,
7B1581E3271FC59C00848B49 /* CallModal.swift */,
7B9F71C828470667006DFE7B /* ReactionListSheet.swift */, 7B9F71C828470667006DFE7B /* ReactionListSheet.swift */,
); );
path = "Views & Modals"; path = "Views & Modals";
@ -5503,7 +5488,6 @@
FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */, FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */,
4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */, 4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */,
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */, 3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */,
C3A76A8D25DB83F90074CB90 /* PermissionMissingModal.swift in Sources */,
7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */, 7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */,
B849789625D4A2F500D0D0B3 /* LinkPreviewView.swift in Sources */, B849789625D4A2F500D0D0B3 /* LinkPreviewView.swift in Sources */,
7B1B52DF28580D51006069F2 /* EmojiPickerCollectionView.swift in Sources */, 7B1B52DF28580D51006069F2 /* EmojiPickerCollectionView.swift in Sources */,
@ -5519,7 +5503,6 @@
4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */, 4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */,
FD87DCFE28B7582C00AF0F98 /* BlockedContactsViewModel.swift in Sources */, FD87DCFE28B7582C00AF0F98 /* BlockedContactsViewModel.swift in Sources */,
FD37E9DD28A384EB003AE748 /* PrimaryColorSelectionView.swift in Sources */, FD37E9DD28A384EB003AE748 /* PrimaryColorSelectionView.swift in Sources */,
B82149B825D60393009C0F2A /* BlockedModal.swift in Sources */,
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */, B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */,
FDFDE126282D05380098B17F /* MediaInteractiveDismiss.swift in Sources */, FDFDE126282D05380098B17F /* MediaInteractiveDismiss.swift in Sources */,
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */, 34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
@ -5531,7 +5514,6 @@
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */, 34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
C331FFFE2558FF3B00070591 /* FullConversationCell.swift in Sources */, C331FFFE2558FF3B00070591 /* FullConversationCell.swift in Sources */,
FD52090728B49738006098F6 /* ConfirmationModal.swift in Sources */, FD52090728B49738006098F6 /* ConfirmationModal.swift in Sources */,
B8F5F72325F1B4CA003BF8D4 /* DownloadAttachmentModal.swift in Sources */,
C3DFFAC623E96F0D0058DAF8 /* Sheet.swift in Sources */, C3DFFAC623E96F0D0058DAF8 /* Sheet.swift in Sources */,
B8D84EA325DF745A005A043E /* LinkPreviewState.swift in Sources */, B8D84EA325DF745A005A043E /* LinkPreviewState.swift in Sources */,
45C0DC1E1E69011F00E04C47 /* UIStoryboard+OWS.swift in Sources */, 45C0DC1E1E69011F00E04C47 /* UIStoryboard+OWS.swift in Sources */,
@ -5540,7 +5522,6 @@
B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */, B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */,
7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */, 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */,
B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */, B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */,
B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */,
B877E24226CA12910007970A /* CallVC.swift in Sources */, B877E24226CA12910007970A /* CallVC.swift in Sources */,
7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */, 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */,
C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */, C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */,
@ -5597,7 +5578,6 @@
4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */, 4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */,
C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */, C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */,
340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */, 340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */,
7B1581E4271FC59D00848B49 /* CallModal.swift in Sources */,
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */, 34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */, B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */,
7B9F71D72853100A006DFE7B /* Emoji+Available.swift in Sources */, 7B9F71D72853100A006DFE7B /* Emoji+Available.swift in Sources */,

View File

@ -1,9 +1,11 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import MediaPlayer
import WebRTC import WebRTC
import SessionUIKit import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SessionUtilitiesKit import SessionUtilitiesKit
import UIKit
import MediaPlayer
final class CallVC: UIViewController, VideoPreviewDelegate { final class CallVC: UIViewController, VideoPreviewDelegate {
let call: SessionCall let call: SessionCall
@ -19,36 +21,51 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
return result return result
}() }()
// MARK: UI Components // MARK: - UI Components
private lazy var localVideoView: LocalVideoView = { private lazy var localVideoView: LocalVideoView = {
let result = LocalVideoView() let result = LocalVideoView()
result.clipsToBounds = true
result.themeBackgroundColor = .backgroundSecondary
result.isHidden = !call.isVideoEnabled result.isHidden = !call.isVideoEnabled
result.layer.cornerRadius = 10 result.layer.cornerRadius = 10
result.layer.masksToBounds = true
result.set(.width, to: LocalVideoView.width) result.set(.width, to: LocalVideoView.width)
result.set(.height, to: LocalVideoView.height) result.set(.height, to: LocalVideoView.height)
result.makeViewDraggable() result.makeViewDraggable()
return result return result
}() }()
private lazy var remoteVideoView: RemoteVideoView = { private lazy var remoteVideoView: RemoteVideoView = {
let result = RemoteVideoView() let result = RemoteVideoView()
result.alpha = 0 result.alpha = 0
result.backgroundColor = .black result.themeBackgroundColor = .backgroundPrimary
result.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleRemoteVieioViewTapped))) result.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleRemoteVieioViewTapped)))
return result return result
}() }()
private lazy var fadeView: UIView = { private lazy var fadeView: UIView = {
let height: CGFloat = ((UIApplication.shared.keyWindow?.safeAreaInsets.top).map { $0 + Values.veryLargeSpacing } ?? 64)
let result = UIView() let result = UIView()
let height: CGFloat = 64
var frame = UIScreen.main.bounds var frame = UIScreen.main.bounds
frame.size.height = height frame.size.height = height
let layer = CAGradientLayer() let layer = CAGradientLayer()
layer.frame = frame layer.frame = frame
layer.colors = [ UIColor(hex: 0x000000).withAlphaComponent(0.4).cgColor, UIColor(hex: 0x000000).withAlphaComponent(0).cgColor ]
result.layer.insertSublayer(layer, at: 0) result.layer.insertSublayer(layer, at: 0)
result.set(.height, to: height) result.set(.height, to: height)
ThemeManager.onThemeChange(observer: result) { [weak layer] theme, _ in
guard let backgroundPrimary: UIColor = theme.colors[.backgroundPrimary] else { return }
layer?.colors = [
backgroundPrimary.withAlphaComponent(0.4).cgColor,
backgroundPrimary.withAlphaComponent(0).cgColor
]
}
return result return result
}() }()
@ -61,42 +78,60 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
result.layer.cornerRadius = radius result.layer.cornerRadius = radius
result.layer.masksToBounds = true result.layer.masksToBounds = true
result.contentMode = .scaleAspectFill result.contentMode = .scaleAspectFill
return result return result
}() }()
private lazy var minimizeButton: UIButton = { private lazy var minimizeButton: UIButton = {
let result = UIButton(type: .custom) let result = UIButton(type: .custom)
result.setImage(
UIImage(named: "Minimize")?
.withRenderingMode(.alwaysTemplate),
for: .normal
)
result.themeTintColor = .textPrimary
result.addTarget(self, action: #selector(minimize), for: UIControl.Event.touchUpInside)
result.isHidden = !call.hasConnected result.isHidden = !call.hasConnected
let image = UIImage(named: "Minimize")!.withTint(.white)
result.setImage(image, for: UIControl.State.normal)
result.set(.width, to: 60) result.set(.width, to: 60)
result.set(.height, to: 60) result.set(.height, to: 60)
result.addTarget(self, action: #selector(minimize), for: UIControl.Event.touchUpInside)
return result return result
}() }()
private lazy var answerButton: UIButton = { private lazy var answerButton: UIButton = {
let result = UIButton(type: .custom) let result = UIButton(type: .custom)
result.isHidden = call.hasStartedConnecting result.setImage(
let image = UIImage(named: "AnswerCall")!.withTint(.white) UIImage(named: "AnswerCall")?
result.setImage(image, for: UIControl.State.normal) .withRenderingMode(.alwaysTemplate),
result.set(.width, to: 60) for: .normal
result.set(.height, to: 60) )
result.backgroundColor = Colors.accent result.themeTintColor = .white
result.themeBackgroundColor = .callAccept_background
result.layer.cornerRadius = 30 result.layer.cornerRadius = 30
result.addTarget(self, action: #selector(answerCall), for: UIControl.Event.touchUpInside) result.addTarget(self, action: #selector(answerCall), for: UIControl.Event.touchUpInside)
result.isHidden = call.hasStartedConnecting
result.set(.width, to: 60)
result.set(.height, to: 60)
return result return result
}() }()
private lazy var hangUpButton: UIButton = { private lazy var hangUpButton: UIButton = {
let result = UIButton(type: .custom) let result = UIButton(type: .custom)
let image = UIImage(named: "EndCall")!.withTint(.white) result.setImage(
result.setImage(image, for: UIControl.State.normal) UIImage(named: "EndCall")?
result.set(.width, to: 60) .withRenderingMode(.alwaysTemplate),
result.set(.height, to: 60) for: .normal
result.backgroundColor = Colors.destructive )
result.themeTintColor = .white
result.themeBackgroundColor = .callDecline_background
result.layer.cornerRadius = 30 result.layer.cornerRadius = 30
result.addTarget(self, action: #selector(endCall), for: UIControl.Event.touchUpInside) result.addTarget(self, action: #selector(endCall), for: UIControl.Event.touchUpInside)
result.set(.width, to: 60)
result.set(.height, to: 60)
return result return result
}() }()
@ -104,58 +139,83 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
let result = UIStackView(arrangedSubviews: [hangUpButton, answerButton]) let result = UIStackView(arrangedSubviews: [hangUpButton, answerButton])
result.axis = .horizontal result.axis = .horizontal
result.spacing = Values.veryLargeSpacing * 2 + 40 result.spacing = Values.veryLargeSpacing * 2 + 40
return result return result
}() }()
private lazy var switchCameraButton: UIButton = { private lazy var switchCameraButton: UIButton = {
let result = UIButton(type: .custom) let result = UIButton(type: .custom)
result.isEnabled = call.isVideoEnabled result.isEnabled = call.isVideoEnabled
let image = UIImage(named: "SwitchCamera")!.withTint(.white) result.setImage(
result.setImage(image, for: UIControl.State.normal) UIImage(named: "SwitchCamera")?
result.set(.width, to: 60) .withRenderingMode(.alwaysTemplate),
result.set(.height, to: 60) for: .normal
result.backgroundColor = UIColor(hex: 0x1F1F1F) )
result.themeTintColor = .textPrimary
result.themeBackgroundColor = .backgroundSecondary
result.layer.cornerRadius = 30 result.layer.cornerRadius = 30
result.addTarget(self, action: #selector(switchCamera), for: UIControl.Event.touchUpInside) result.addTarget(self, action: #selector(switchCamera), for: UIControl.Event.touchUpInside)
result.set(.width, to: 60)
result.set(.height, to: 60)
return result return result
}() }()
private lazy var switchAudioButton: UIButton = { private lazy var switchAudioButton: UIButton = {
let result = UIButton(type: .custom) let result = UIButton(type: .custom)
let image = UIImage(named: "AudioOff")!.withTint(.white) result.setImage(
result.setImage(image, for: UIControl.State.normal) UIImage(named: "AudioOff")?
result.set(.width, to: 60) .withRenderingMode(.alwaysTemplate),
result.set(.height, to: 60) for: .normal
result.backgroundColor = call.isMuted ? Colors.destructive : UIColor(hex: 0x1F1F1F) )
result.themeTintColor = (call.isMuted ?
.white :
.textPrimary
)
result.themeBackgroundColor = (call.isMuted ?
.danger :
.backgroundSecondary
)
result.layer.cornerRadius = 30 result.layer.cornerRadius = 30
result.addTarget(self, action: #selector(switchAudio), for: UIControl.Event.touchUpInside) result.addTarget(self, action: #selector(switchAudio), for: UIControl.Event.touchUpInside)
result.set(.width, to: 60)
result.set(.height, to: 60)
return result return result
}() }()
private lazy var videoButton: UIButton = { private lazy var videoButton: UIButton = {
let result = UIButton(type: .custom) let result = UIButton(type: .custom)
let image = UIImage(named: "VideoCall")?.withRenderingMode(.alwaysTemplate) result.setImage(
result.setImage(image, for: UIControl.State.normal) UIImage(named: "VideoCall")?
result.set(.width, to: 60) .withRenderingMode(.alwaysTemplate),
result.set(.height, to: 60) for: .normal
result.tintColor = .white )
result.backgroundColor = UIColor(hex: 0x1F1F1F) result.themeTintColor = .textPrimary
result.themeBackgroundColor = .backgroundSecondary
result.layer.cornerRadius = 30 result.layer.cornerRadius = 30
result.addTarget(self, action: #selector(operateCamera), for: UIControl.Event.touchUpInside) result.addTarget(self, action: #selector(operateCamera), for: UIControl.Event.touchUpInside)
result.set(.width, to: 60)
result.set(.height, to: 60)
return result return result
}() }()
private lazy var volumeView: MPVolumeView = { private lazy var volumeView: MPVolumeView = {
let result = MPVolumeView() let result = MPVolumeView()
let image = UIImage(named: "Speaker")?.withRenderingMode(.alwaysTemplate)
result.showsVolumeSlider = false result.showsVolumeSlider = false
result.showsRouteButton = true result.showsRouteButton = true
result.setRouteButtonImage(image, for: UIControl.State.normal) result.setRouteButtonImage(
UIImage(named: "Speaker")?
.withRenderingMode(.alwaysTemplate),
for: .normal
)
result.themeTintColor = .textPrimary
result.themeBackgroundColor = .backgroundSecondary
result.layer.cornerRadius = 30
result.set(.width, to: 60) result.set(.width, to: 60)
result.set(.height, to: 60) result.set(.height, to: 60)
result.tintColor = .white
result.backgroundColor = UIColor(hex: 0x1F1F1F)
result.layer.cornerRadius = 30
return result return result
}() }()
@ -163,37 +223,43 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
let result = UIStackView(arrangedSubviews: [switchCameraButton, videoButton, switchAudioButton, volumeView]) let result = UIStackView(arrangedSubviews: [switchCameraButton, videoButton, switchAudioButton, volumeView])
result.axis = .horizontal result.axis = .horizontal
result.spacing = Values.veryLargeSpacing result.spacing = Values.veryLargeSpacing
return result return result
}() }()
private lazy var titleLabel: UILabel = { private lazy var titleLabel: UILabel = {
let result = UILabel() let result: UILabel = UILabel()
result.textColor = .white
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize) result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
result.themeTextColor = .textPrimary
result.textAlignment = .center result.textAlignment = .center
return result return result
}() }()
private lazy var callInfoLabel: UILabel = { private lazy var callInfoLabel: UILabel = {
let result = UILabel() let result: UILabel = UILabel()
result.isHidden = call.hasConnected
result.textColor = .white
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize) result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
result.themeTextColor = .textPrimary
result.textAlignment = .center result.textAlignment = .center
result.isHidden = call.hasConnected
if call.hasStartedConnecting { result.text = "Connecting..." } if call.hasStartedConnecting { result.text = "Connecting..." }
return result return result
}() }()
private lazy var callDurationLabel: UILabel = { private lazy var callDurationLabel: UILabel = {
let result = UILabel() let result = UILabel()
result.isHidden = true
result.textColor = .white
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize) result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
result.themeTextColor = .textPrimary
result.textAlignment = .center result.textAlignment = .center
result.isHidden = true
return result return result
}() }()
// MARK: Lifecycle // MARK: - Lifecycle
init(for call: SessionCall) { init(for call: SessionCall) {
self.call = call self.call = call
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
@ -208,6 +274,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
UIView.animate(withDuration: 0.25) { UIView.animate(withDuration: 0.25) {
self.remoteVideoView.alpha = isEnabled ? 1 : 0 self.remoteVideoView.alpha = isEnabled ? 1 : 0
} }
if self.callInfoLabel.alpha < 0.5 { if self.callInfoLabel.alpha < 0.5 {
UIView.animate(withDuration: 0.25) { UIView.animate(withDuration: 0.25) {
self.operationPanel.alpha = 1 self.operationPanel.alpha = 1
@ -217,45 +284,60 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
} }
} }
} }
self.call.hasStartedConnectingDidChange = { self.call.hasStartedConnectingDidChange = {
DispatchQueue.main.async { DispatchQueue.main.async {
self.callInfoLabel.text = "Connecting..." self.callInfoLabel.text = "Connecting..."
self.answerButton.alpha = 0 self.answerButton.alpha = 0
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: {
self.answerButton.isHidden = true UIView.animate(
}, completion: nil) withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: 1,
options: .curveEaseIn,
animations: { [weak self] in
self?.answerButton.isHidden = true
},
completion: nil
)
} }
} }
self.call.hasConnectedDidChange = {
self.call.hasConnectedDidChange = { [weak self] in
DispatchQueue.main.async { DispatchQueue.main.async {
CallRingTonePlayer.shared.stopPlayingRingTone() CallRingTonePlayer.shared.stopPlayingRingTone()
self.callInfoLabel.text = "Connected"
self.minimizeButton.isHidden = false self?.callInfoLabel.text = "Connected"
self.durationTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in self?.minimizeButton.isHidden = false
self.updateDuration() self?.durationTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self?.updateDuration()
} }
self.callInfoLabel.isHidden = true self?.callInfoLabel.isHidden = true
self.callDurationLabel.isHidden = false self?.callDurationLabel.isHidden = false
} }
} }
self.call.hasEndedDidChange = {
self.call.hasEndedDidChange = { [weak self] in
DispatchQueue.main.async { DispatchQueue.main.async {
self.durationTimer?.invalidate() self?.durationTimer?.invalidate()
self.durationTimer = nil self?.durationTimer = nil
self.handleEndCallMessage() self?.handleEndCallMessage()
} }
} }
self.call.hasStartedReconnecting = {
self.call.hasStartedReconnecting = { [weak self] in
DispatchQueue.main.async { DispatchQueue.main.async {
self.callInfoLabel.isHidden = false self?.callInfoLabel.isHidden = false
self.callDurationLabel.isHidden = true self?.callDurationLabel.isHidden = true
self.callInfoLabel.text = "Reconnecting..." self?.callInfoLabel.text = "Reconnecting..."
} }
} }
self.call.hasReconnected = {
self.call.hasReconnected = { [weak self] in
DispatchQueue.main.async { DispatchQueue.main.async {
self.callInfoLabel.isHidden = true self?.callInfoLabel.isHidden = true
self.callDurationLabel.isHidden = false self?.callDurationLabel.isHidden = false
} }
} }
} }
@ -264,19 +346,24 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .black
view.themeBackgroundColor = .backgroundPrimary
setUpViewHierarchy() setUpViewHierarchy()
if shouldRestartCamera { cameraManager.prepare() } if shouldRestartCamera { cameraManager.prepare() }
touch(call.videoCapturer) touch(call.videoCapturer)
titleLabel.text = self.call.contactName titleLabel.text = self.call.contactName
AppEnvironment.shared.callManager.startCall(call) { error in AppEnvironment.shared.callManager.startCall(call) { [weak self] error in
DispatchQueue.main.async { DispatchQueue.main.async {
if let _ = error { if let _ = error {
self.callInfoLabel.text = "Can't start a call." self?.callInfoLabel.text = "Can't start a call."
self.endCall() self?.endCall()
} else { }
self.callInfoLabel.text = "Ringing..." else {
self.answerButton.isHidden = true self?.callInfoLabel.text = "Ringing..."
self?.answerButton.isHidden = true
} }
} }
} }
@ -293,41 +380,50 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
// Profile picture container // Profile picture container
let profilePictureContainer = UIView() let profilePictureContainer = UIView()
view.addSubview(profilePictureContainer) view.addSubview(profilePictureContainer)
// Remote video view // Remote video view
call.attachRemoteVideoRenderer(remoteVideoView) call.attachRemoteVideoRenderer(remoteVideoView)
view.addSubview(remoteVideoView) view.addSubview(remoteVideoView)
remoteVideoView.translatesAutoresizingMaskIntoConstraints = false remoteVideoView.translatesAutoresizingMaskIntoConstraints = false
remoteVideoView.pin(to: view) remoteVideoView.pin(to: view)
// Local video view // Local video view
call.attachLocalVideoRenderer(localVideoView) call.attachLocalVideoRenderer(localVideoView)
// Fade view // Fade view
view.addSubview(fadeView) view.addSubview(fadeView)
fadeView.translatesAutoresizingMaskIntoConstraints = false fadeView.translatesAutoresizingMaskIntoConstraints = false
fadeView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: view) fadeView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: view)
// Minimize button // Minimize button
view.addSubview(minimizeButton) view.addSubview(minimizeButton)
minimizeButton.translatesAutoresizingMaskIntoConstraints = false minimizeButton.translatesAutoresizingMaskIntoConstraints = false
minimizeButton.pin(.left, to: .left, of: view) minimizeButton.pin(.left, to: .left, of: view)
minimizeButton.pin(.top, to: .top, of: view, withInset: 32) minimizeButton.pin(.top, to: .top, of: view, withInset: 32)
// Title label // Title label
view.addSubview(titleLabel) view.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.center(.vertical, in: minimizeButton) titleLabel.center(.vertical, in: minimizeButton)
titleLabel.center(.horizontal, in: view) titleLabel.center(.horizontal, in: view)
// Response Panel // Response Panel
view.addSubview(responsePanel) view.addSubview(responsePanel)
responsePanel.center(.horizontal, in: view) responsePanel.center(.horizontal, in: view)
responsePanel.pin(.bottom, to: .bottom, of: view, withInset: -Values.newConversationButtonBottomOffset) responsePanel.pin(.bottom, to: .bottom, of: view, withInset: -Values.newConversationButtonBottomOffset)
// Operation Panel // Operation Panel
view.addSubview(operationPanel) view.addSubview(operationPanel)
operationPanel.center(.horizontal, in: view) operationPanel.center(.horizontal, in: view)
operationPanel.pin(.bottom, to: .top, of: responsePanel, withInset: -Values.veryLargeSpacing) operationPanel.pin(.bottom, to: .top, of: responsePanel, withInset: -Values.veryLargeSpacing)
// Profile picture view // Profile picture view
profilePictureContainer.pin(.top, to: .bottom, of: fadeView) profilePictureContainer.pin(.top, to: .bottom, of: fadeView)
profilePictureContainer.pin(.bottom, to: .top, of: operationPanel) profilePictureContainer.pin(.bottom, to: .top, of: operationPanel)
profilePictureContainer.pin([ UIView.HorizontalEdge.left, UIView.HorizontalEdge.right ], to: view) profilePictureContainer.pin([ UIView.HorizontalEdge.left, UIView.HorizontalEdge.right ], to: view)
profilePictureContainer.addSubview(profilePictureView) profilePictureContainer.addSubview(profilePictureView)
profilePictureView.center(in: profilePictureContainer) profilePictureView.center(in: profilePictureContainer)
// Call info label // Call info label
let callInfoLabelContainer = UIView() let callInfoLabelContainer = UIView()
view.addSubview(callInfoLabelContainer) view.addSubview(callInfoLabelContainer)
@ -343,25 +439,28 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
} }
private func addLocalVideoView() { private func addLocalVideoView() {
let safeAreaInsets = UIApplication.shared.keyWindow!.safeAreaInsets let safeAreaInsets = UIApplication.shared.keyWindow?.safeAreaInsets
let window = CurrentAppContext().mainWindow! CurrentAppContext().mainWindow?.addSubview(localVideoView)
window.addSubview(localVideoView)
localVideoView.autoPinEdge(toSuperviewEdge: .right, withInset: Values.smallSpacing) localVideoView.autoPinEdge(toSuperviewEdge: .right, withInset: Values.smallSpacing)
let topMargin = safeAreaInsets.top + Values.veryLargeSpacing let topMargin = (safeAreaInsets?.top ?? 0) + Values.veryLargeSpacing
localVideoView.autoPinEdge(toSuperviewEdge: .top, withInset: topMargin) localVideoView.autoPinEdge(toSuperviewEdge: .top, withInset: topMargin)
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.start() } if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.start() }
shouldRestartCamera = true shouldRestartCamera = true
addLocalVideoView() addLocalVideoView()
remoteVideoView.alpha = call.isRemoteVideoEnabled ? 1 : 0 remoteVideoView.alpha = (call.isRemoteVideoEnabled ? 1 : 0)
} }
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.stop() } if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.stop() }
localVideoView.removeFromSuperview() localVideoView.removeFromSuperview()
} }
@ -373,9 +472,9 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
} }
@objc func didChangeDeviceOrientation(notification: Notification) { @objc func didChangeDeviceOrientation(notification: Notification) {
func rotateAllButtons(rotationAngle: CGFloat) { func rotateAllButtons(rotationAngle: CGFloat) {
let transform = CGAffineTransform(rotationAngle: rotationAngle) let transform = CGAffineTransform(rotationAngle: rotationAngle)
UIView.animate(withDuration: 0.2) { UIView.animate(withDuration: 0.2) {
self.answerButton.transform = transform self.answerButton.transform = transform
self.hangUpButton.transform = transform self.hangUpButton.transform = transform
@ -387,16 +486,11 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
} }
switch UIDevice.current.orientation { switch UIDevice.current.orientation {
case .portrait: case .portrait: rotateAllButtons(rotationAngle: 0)
rotateAllButtons(rotationAngle: 0) case .portraitUpsideDown: rotateAllButtons(rotationAngle: .pi)
case .portraitUpsideDown: case .landscapeLeft: rotateAllButtons(rotationAngle: .halfPi)
rotateAllButtons(rotationAngle: .pi) case .landscapeRight: rotateAllButtons(rotationAngle: .pi + .halfPi)
case .landscapeLeft: default: break
rotateAllButtons(rotationAngle: .halfPi)
case .landscapeRight:
rotateAllButtons(rotationAngle: .pi + .halfPi)
default:
break
} }
} }
@ -409,39 +503,42 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
SNLog("[Calls] Ending call.") SNLog("[Calls] Ending call.")
self.callInfoLabel.isHidden = false self.callInfoLabel.isHidden = false
self.callDurationLabel.isHidden = true self.callDurationLabel.isHidden = true
callInfoLabel.text = "Call Ended" self.callInfoLabel.text = "Call Ended"
UIView.animate(withDuration: 0.25) { UIView.animate(withDuration: 0.25) {
self.remoteVideoView.alpha = 0 self.remoteVideoView.alpha = 0
self.operationPanel.alpha = 1 self.operationPanel.alpha = 1
self.responsePanel.alpha = 1 self.responsePanel.alpha = 1
self.callInfoLabel.alpha = 1 self.callInfoLabel.alpha = 1
} }
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
self.conversationVC?.showInputAccessoryView() Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { [weak self] _ in
self.presentingViewController?.dismiss(animated: true, completion: nil) self?.conversationVC?.showInputAccessoryView()
self?.presentingViewController?.dismiss(animated: true, completion: nil)
} }
} }
@objc private func answerCall() { @objc private func answerCall() {
AppEnvironment.shared.callManager.answerCall(call) { error in AppEnvironment.shared.callManager.answerCall(call) { [weak self] error in
DispatchQueue.main.async { DispatchQueue.main.async {
if let _ = error { if let _ = error {
self.callInfoLabel.text = "Can't answer the call." self?.callInfoLabel.text = "Can't answer the call."
self.endCall() self?.endCall()
} }
} }
} }
} }
@objc private func endCall() { @objc private func endCall() {
AppEnvironment.shared.callManager.endCall(call) { error in AppEnvironment.shared.callManager.endCall(call) { [weak self] error in
if let _ = error { if let _ = error {
self.call.endSessionCall() self?.call.endSessionCall()
AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: nil) AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: nil)
} }
DispatchQueue.main.async { DispatchQueue.main.async {
self.conversationVC?.showInputAccessoryView() self?.conversationVC?.showInputAccessoryView()
self.presentingViewController?.dismiss(animated: true, completion: nil) self?.presentingViewController?.dismiss(animated: true, completion: nil)
} }
} }
} }
@ -451,7 +548,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
duration += 1 duration += 1
} }
// MARK: Minimize to a floating view // MARK: - Minimize to a floating view
@objc private func minimize() { @objc private func minimize() {
self.shouldRestartCamera = false self.shouldRestartCamera = false
let miniCallView = MiniCallView(from: self) let miniCallView = MiniCallView(from: self)
@ -460,17 +558,19 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
presentingViewController?.dismiss(animated: true, completion: nil) presentingViewController?.dismiss(animated: true, completion: nil)
} }
// MARK: Video and Audio // MARK: - Video and Audio
@objc private func operateCamera() { @objc private func operateCamera() {
if (call.isVideoEnabled) { if (call.isVideoEnabled) {
localVideoView.isHidden = true localVideoView.isHidden = true
cameraManager.stop() cameraManager.stop()
videoButton.tintColor = .white videoButton.themeTintColor = .textPrimary
videoButton.backgroundColor = UIColor(hex: 0x1F1F1F) videoButton.themeBackgroundColor = .backgroundSecondary
switchCameraButton.isEnabled = false switchCameraButton.isEnabled = false
call.isVideoEnabled = false call.isVideoEnabled = false
} else { }
guard requestCameraPermissionIfNeeded() else { return } else {
guard Permissions.requestCameraPermissionIfNeeded() else { return }
let previewVC = VideoPreviewVC() let previewVC = VideoPreviewVC()
previewVC.delegate = self previewVC.delegate = self
present(previewVC, animated: true, completion: nil) present(previewVC, animated: true, completion: nil)
@ -481,8 +581,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
localVideoView.isHidden = false localVideoView.isHidden = false
cameraManager.prepare() cameraManager.prepare()
cameraManager.start() cameraManager.start()
videoButton.tintColor = UIColor(hex: 0x1F1F1F) videoButton.themeTintColor = .backgroundSecondary
videoButton.backgroundColor = .white videoButton.themeBackgroundColor = .textPrimary
switchCameraButton.isEnabled = true switchCameraButton.isEnabled = true
call.isVideoEnabled = true call.isVideoEnabled = true
} }
@ -493,10 +593,13 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
@objc private func switchAudio() { @objc private func switchAudio() {
if call.isMuted { if call.isMuted {
switchAudioButton.backgroundColor = UIColor(hex: 0x1F1F1F) switchAudioButton.themeTintColor = .textPrimary
switchAudioButton.themeBackgroundColor = .backgroundSecondary
call.isMuted = false call.isMuted = false
} else { }
switchAudioButton.backgroundColor = Colors.destructive else {
switchAudioButton.themeTintColor = .white
switchAudioButton.themeBackgroundColor = .danger
call.isMuted = true call.isMuted = true
} }
} }
@ -506,41 +609,48 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
let currentRoute = currentSession.currentRoute let currentRoute = currentSession.currentRoute
if let currentOutput = currentRoute.outputs.first { if let currentOutput = currentRoute.outputs.first {
if let latestKnownAudioOutputDeviceName = latestKnownAudioOutputDeviceName, currentOutput.portName == latestKnownAudioOutputDeviceName { return } if let latestKnownAudioOutputDeviceName = latestKnownAudioOutputDeviceName, currentOutput.portName == latestKnownAudioOutputDeviceName { return }
latestKnownAudioOutputDeviceName = currentOutput.portName latestKnownAudioOutputDeviceName = currentOutput.portName
switch currentOutput.portType { switch currentOutput.portType {
case .builtInSpeaker: case .builtInSpeaker:
let image = UIImage(named: "Speaker")?.withRenderingMode(.alwaysTemplate) let image = UIImage(named: "Speaker")?.withRenderingMode(.alwaysTemplate)
volumeView.setRouteButtonImage(image, for: .normal) volumeView.setRouteButtonImage(image, for: .normal)
volumeView.tintColor = UIColor(hex: 0x1F1F1F) volumeView.themeTintColor = .backgroundSecondary
volumeView.backgroundColor = .white volumeView.themeBackgroundColor = .textPrimary
case .headphones:
let image = UIImage(named: "Headsets")?.withRenderingMode(.alwaysTemplate) case .headphones:
volumeView.setRouteButtonImage(image, for: .normal) let image = UIImage(named: "Headsets")?.withRenderingMode(.alwaysTemplate)
volumeView.tintColor = UIColor(hex: 0x1F1F1F) volumeView.setRouteButtonImage(image, for: .normal)
volumeView.backgroundColor = .white volumeView.themeTintColor = .backgroundSecondary
case .bluetoothLE: fallthrough volumeView.themeBackgroundColor = .textPrimary
case .bluetoothA2DP:
let image = UIImage(named: "Bluetooth")?.withRenderingMode(.alwaysTemplate) case .bluetoothLE: fallthrough
volumeView.setRouteButtonImage(image, for: .normal) case .bluetoothA2DP:
volumeView.tintColor = UIColor(hex: 0x1F1F1F) let image = UIImage(named: "Bluetooth")?.withRenderingMode(.alwaysTemplate)
volumeView.backgroundColor = .white volumeView.setRouteButtonImage(image, for: .normal)
case .bluetoothHFP: volumeView.themeTintColor = .backgroundSecondary
let image = UIImage(named: "Airpods")?.withRenderingMode(.alwaysTemplate) volumeView.themeBackgroundColor = .textPrimary
volumeView.setRouteButtonImage(image, for: .normal)
volumeView.tintColor = UIColor(hex: 0x1F1F1F) case .bluetoothHFP:
volumeView.backgroundColor = .white let image = UIImage(named: "Airpods")?.withRenderingMode(.alwaysTemplate)
case .builtInReceiver: fallthrough volumeView.setRouteButtonImage(image, for: .normal)
default: volumeView.themeTintColor = .backgroundSecondary
let image = UIImage(named: "Speaker")?.withRenderingMode(.alwaysTemplate) volumeView.themeBackgroundColor = .textPrimary
volumeView.setRouteButtonImage(image, for: .normal)
volumeView.tintColor = .white case .builtInReceiver: fallthrough
volumeView.backgroundColor = UIColor(hex: 0x1F1F1F) default:
let image = UIImage(named: "Speaker")?.withRenderingMode(.alwaysTemplate)
volumeView.setRouteButtonImage(image, for: .normal)
volumeView.themeTintColor = .backgroundSecondary
volumeView.themeBackgroundColor = .textPrimary
} }
} }
} }
@objc private func handleRemoteVieioViewTapped(gesture: UITapGestureRecognizer) { @objc private func handleRemoteVieioViewTapped(gesture: UITapGestureRecognizer) {
let isHidden = callDurationLabel.alpha < 0.5 let isHidden = callDurationLabel.alpha < 0.5
UIView.animate(withDuration: 0.5) { UIView.animate(withDuration: 0.5) {
self.operationPanel.alpha = isHidden ? 1 : 0 self.operationPanel.alpha = isHidden ? 1 : 0
self.responsePanel.alpha = isHidden ? 1 : 0 self.responsePanel.alpha = isHidden ? 1 : 0

View File

@ -47,16 +47,24 @@ final class CameraManager : NSObject {
func start() { func start() {
guard !isCapturing else { return } guard !isCapturing else { return }
print("[Calls] Starting camera.")
isCapturing = true // Note: The 'startRunning' task is blocking so we want to do it on a non-main thread
captureSession.startRunning() DispatchQueue.global(qos: .userInitiated).async { [weak self] in
print("[Calls] Starting camera.")
self?.isCapturing = true
self?.captureSession.startRunning()
}
} }
func stop() { func stop() {
guard isCapturing else { return } guard isCapturing else { return }
print("[Calls] Stopping camera.")
isCapturing = false // Note: The 'stopRunning' task is blocking so we want to do it on a non-main thread
captureSession.stopRunning() DispatchQueue.global(qos: .userInitiated).async { [weak self] in
print("[Calls] Stopping camera.")
self?.isCapturing = false
self?.captureSession.stopRunning()
}
} }
func switchCamera() { func switchCamera() {

View File

@ -1,7 +1,10 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit import UIKit
import WebRTC import WebRTC
import SessionUIKit
public protocol VideoPreviewDelegate : AnyObject { public protocol VideoPreviewDelegate: AnyObject {
func cameraDidConfirmTurningOn() func cameraDidConfirmTurningOn()
} }
@ -11,61 +14,89 @@ class VideoPreviewVC: UIViewController, CameraManagerDelegate {
lazy var cameraManager: CameraManager = { lazy var cameraManager: CameraManager = {
let result = CameraManager() let result = CameraManager()
result.delegate = self result.delegate = self
return result return result
}() }()
// MARK: UI Components // MARK: - UI Components
private lazy var renderView: RenderView = { private lazy var renderView: RenderView = {
let result = RenderView() let result = RenderView()
return result return result
}() }()
private lazy var fadeView: UIView = { private lazy var fadeView: UIView = {
let height: CGFloat = ((UIApplication.shared.keyWindow?.safeAreaInsets.top).map { $0 + Values.veryLargeSpacing } ?? 64)
let result = UIView() let result = UIView()
let height: CGFloat = 64
var frame = UIScreen.main.bounds var frame = UIScreen.main.bounds
frame.size.height = height frame.size.height = height
let layer = CAGradientLayer() let layer = CAGradientLayer()
layer.frame = frame layer.frame = frame
layer.colors = [ UIColor(hex: 0x000000).withAlphaComponent(0.4).cgColor, UIColor(hex: 0x000000).withAlphaComponent(0).cgColor ]
result.layer.insertSublayer(layer, at: 0) result.layer.insertSublayer(layer, at: 0)
result.set(.height, to: height) result.set(.height, to: height)
ThemeManager.onThemeChange(observer: result) { [weak layer] theme, _ in
guard let backgroundPrimary: UIColor = theme.colors[.backgroundPrimary] else { return }
layer?.colors = [
backgroundPrimary.withAlphaComponent(0.4).cgColor,
backgroundPrimary.withAlphaComponent(0).cgColor
]
}
return result return result
}() }()
private lazy var closeButton: UIButton = { private lazy var closeButton: UIButton = {
let result = UIButton(type: .custom) let result = UIButton(type: .custom)
let image = UIImage(named: "X")!.withTint(.white) result.setImage(
result.setImage(image, for: UIControl.State.normal) UIImage(named: "X")?
.withRenderingMode(.alwaysTemplate),
for: .normal
)
result.themeTintColor = .textPrimary
result.addTarget(self, action: #selector(cancel), for: UIControl.Event.touchUpInside)
result.set(.width, to: 60) result.set(.width, to: 60)
result.set(.height, to: 60) result.set(.height, to: 60)
result.addTarget(self, action: #selector(cancel), for: UIControl.Event.touchUpInside)
return result return result
}() }()
private lazy var confirmButton: UIButton = { private lazy var confirmButton: UIButton = {
let result = UIButton(type: .custom) let result = UIButton(type: .custom)
let image = UIImage(named: "Check")!.withTint(.white) result.setImage(
result.setImage(image, for: UIControl.State.normal) UIImage(named: "Check")?
.withRenderingMode(.alwaysTemplate),
for: .normal
)
result.themeTintColor = .textPrimary
result.addTarget(self, action: #selector(confirm), for: UIControl.Event.touchUpInside)
result.set(.width, to: 60) result.set(.width, to: 60)
result.set(.height, to: 60) result.set(.height, to: 60)
result.addTarget(self, action: #selector(confirm), for: UIControl.Event.touchUpInside)
return result return result
}() }()
private lazy var titleLabel: UILabel = { private lazy var titleLabel: UILabel = {
let result = UILabel() let result = UILabel()
result.text = "Preview"
result.textColor = .white
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize) result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
result.text = "Preview"
result.themeTextColor = .textPrimary
result.textAlignment = .center result.textAlignment = .center
return result return result
}() }()
// MARK: Lifecycle // MARK: - Lifecycle
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .black
view.themeBackgroundColor = .backgroundPrimary
setUpViewHierarchy() setUpViewHierarchy()
cameraManager.prepare() cameraManager.prepare()
} }
@ -75,20 +106,24 @@ class VideoPreviewVC: UIViewController, CameraManagerDelegate {
view.addSubview(renderView) view.addSubview(renderView)
renderView.translatesAutoresizingMaskIntoConstraints = false renderView.translatesAutoresizingMaskIntoConstraints = false
renderView.pin(to: view) renderView.pin(to: view)
// Fade view // Fade view
view.addSubview(fadeView) view.addSubview(fadeView)
fadeView.translatesAutoresizingMaskIntoConstraints = false fadeView.translatesAutoresizingMaskIntoConstraints = false
fadeView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: view) fadeView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: view)
// Close button // Close button
view.addSubview(closeButton) view.addSubview(closeButton)
closeButton.translatesAutoresizingMaskIntoConstraints = false closeButton.translatesAutoresizingMaskIntoConstraints = false
closeButton.pin(.left, to: .left, of: view) closeButton.pin(.left, to: .left, of: view)
closeButton.center(.vertical, in: fadeView) closeButton.center(.vertical, in: fadeView)
// Confirm button // Confirm button
view.addSubview(confirmButton) view.addSubview(confirmButton)
confirmButton.translatesAutoresizingMaskIntoConstraints = false confirmButton.translatesAutoresizingMaskIntoConstraints = false
confirmButton.pin(.right, to: .right, of: view) confirmButton.pin(.right, to: .right, of: view)
confirmButton.center(.vertical, in: fadeView) confirmButton.center(.vertical, in: fadeView)
// Title label // Title label
view.addSubview(titleLabel) view.addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.translatesAutoresizingMaskIntoConstraints = false
@ -98,15 +133,18 @@ class VideoPreviewVC: UIViewController, CameraManagerDelegate {
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
cameraManager.start() cameraManager.start()
} }
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
cameraManager.stop() cameraManager.stop()
} }
// MARK: Interaction // MARK: - Interaction
@objc func confirm() { @objc func confirm() {
delegate?.cameraDidConfirmTurningOn() delegate?.cameraDidConfirmTurningOn()
self.dismiss(animated: true, completion: nil) self.dismiss(animated: true, completion: nil)
@ -116,7 +154,8 @@ class VideoPreviewVC: UIViewController, CameraManagerDelegate {
self.dismiss(animated: true, completion: nil) self.dismiss(animated: true, completion: nil)
} }
// MARK: CameraManagerDelegate // MARK: - CameraManagerDelegate
func handleVideoOutputCaptured(sampleBuffer: CMSampleBuffer) { func handleVideoOutputCaptured(sampleBuffer: CMSampleBuffer) {
renderView.enqueue(sampleBuffer: sampleBuffer) renderView.enqueue(sampleBuffer: sampleBuffer)
} }

View File

@ -71,18 +71,14 @@ final class CallMissedTipsModal: Modal {
init(caller: String) { init(caller: String) {
self.caller = caller self.caller = caller
super.init(nibName: nil, bundle: nil) super.init()
self.modalPresentationStyle = .overFullScreen self.modalPresentationStyle = .overFullScreen
self.modalTransitionStyle = .crossDissolve self.modalTransitionStyle = .crossDissolve
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
preconditionFailure("Use init(onCallEnabled:) instead.") preconditionFailure("Use init(caller:) instead.")
}
override init(nibName: String?, bundle: Bundle?) {
preconditionFailure("Use init(onCallEnabled:) instead.")
} }
override func populateContentView() { override func populateContentView() {

View File

@ -1,13 +1,16 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit import UIKit
import WebRTC import WebRTC
import SessionUIKit
final class MiniCallView: UIView, RTCVideoViewDelegate { final class MiniCallView: UIView, RTCVideoViewDelegate {
var callVC: CallVC var callVC: CallVC
// MARK: UI // MARK: UI
private static let defaultSize: CGFloat = 100 private static let defaultSize: CGFloat = 100
private let topMargin = UIApplication.shared.keyWindow!.safeAreaInsets.top + Values.veryLargeSpacing private let topMargin = (UIApplication.shared.keyWindow?.safeAreaInsets.top ?? 0) + Values.veryLargeSpacing
private let bottomMargin = UIApplication.shared.keyWindow!.safeAreaInsets.bottom private let bottomMargin = (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0)
private var width: NSLayoutConstraint? private var width: NSLayoutConstraint?
private var height: NSLayoutConstraint? private var height: NSLayoutConstraint?
@ -16,40 +19,54 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
private var top: NSLayoutConstraint? private var top: NSLayoutConstraint?
private var bottom: NSLayoutConstraint? private var bottom: NSLayoutConstraint?
private let backgroundView: UIView = {
let result: UIView = UIView()
result.themeBackgroundColor = .textPrimary
result.alpha = 0.8
return result
}()
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
// Note: 'RTCMTLVideoView' doesn't seem to work on the simulator so use 'RTCEAGLVideoView' instead // Note: 'RTCMTLVideoView' doesn't seem to work on the simulator so use 'RTCEAGLVideoView' instead
private lazy var remoteVideoView: RTCEAGLVideoView = { private lazy var remoteVideoView: RTCEAGLVideoView = {
let result = RTCEAGLVideoView() let result = RTCEAGLVideoView()
result.delegate = self result.delegate = self
result.alpha = self.callVC.call.isRemoteVideoEnabled ? 1 : 0 result.themeBackgroundColor = .backgroundSecondary
result.backgroundColor = .black result.alpha = (self.callVC.call.isRemoteVideoEnabled ? 1 : 0)
return result return result
}() }()
#else #else
private lazy var remoteVideoView: RTCMTLVideoView = { private lazy var remoteVideoView: RTCMTLVideoView = {
let result = RTCMTLVideoView() let result = RTCMTLVideoView()
result.delegate = self result.delegate = self
result.alpha = self.callVC.call.isRemoteVideoEnabled ? 1 : 0
result.videoContentMode = .scaleAspectFit result.videoContentMode = .scaleAspectFit
result.backgroundColor = .black result.themeBackgroundColor = .backgroundSecondary
result.alpha = (self.callVC.call.isRemoteVideoEnabled ? 1 : 0)
return result return result
}() }()
#endif #endif
// MARK: Initialization // MARK: - Initialization
public static var current: MiniCallView? public static var current: MiniCallView?
init(from callVC: CallVC) { init(from callVC: CallVC) {
self.callVC = callVC self.callVC = callVC
super.init(frame: CGRect.zero) super.init(frame: CGRect.zero)
self.backgroundColor = UIColor.init(white: 0, alpha: 0.8)
setUpViewHierarchy() setUpViewHierarchy()
setUpGestureRecognizers() setUpGestureRecognizers()
MiniCallView.current = self MiniCallView.current = self
self.callVC.call.remoteVideoStateDidChange = { isEnabled in self.callVC.call.remoteVideoStateDidChange = { isEnabled in
DispatchQueue.main.async { DispatchQueue.main.async {
UIView.animate(withDuration: 0.25) { UIView.animate(withDuration: 0.25) {
self.remoteVideoView.alpha = isEnabled ? 1 : 0 self.remoteVideoView.alpha = isEnabled ? 1 : 0
if !isEnabled { if !isEnabled {
self.width?.constant = MiniCallView.defaultSize self.width?.constant = MiniCallView.defaultSize
self.height?.constant = MiniCallView.defaultSize self.height?.constant = MiniCallView.defaultSize
@ -57,6 +74,13 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
} }
} }
} }
NotificationCenter.default.addObserver(
self,
selector: #selector(windowSubviewsChanged),
name: .windowSubviewsChanged,
object: nil
)
} }
override init(frame: CGRect) { override init(frame: CGRect) {
@ -67,15 +91,21 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
preconditionFailure("Use init(coder:) instead.") preconditionFailure("Use init(coder:) instead.")
} }
deinit {
NotificationCenter.default.removeObserver(self)
}
private func setUpViewHierarchy() { private func setUpViewHierarchy() {
self.clipsToBounds = true
self.layer.cornerRadius = 10
self.width = self.set(.width, to: MiniCallView.defaultSize) self.width = self.set(.width, to: MiniCallView.defaultSize)
self.height = self.set(.height, to: MiniCallView.defaultSize) self.height = self.set(.height, to: MiniCallView.defaultSize)
self.layer.cornerRadius = 10
self.layer.masksToBounds = true
// Background // Background
let background = getBackgroudView() let background = getBackgroudView()
self.addSubview(background) self.addSubview(background)
background.pin(to: self) background.pin(to: self)
// Remote video view // Remote video view
callVC.call.attachRemoteVideoRenderer(remoteVideoView) callVC.call.attachRemoteVideoRenderer(remoteVideoView)
self.addSubview(remoteVideoView) self.addSubview(remoteVideoView)
@ -84,17 +114,25 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
} }
private func getBackgroudView() -> UIView { private func getBackgroudView() -> UIView {
let background = UIView() let result: UIView = UIView()
let background: UIView = UIView()
background.themeBackgroundColor = .textPrimary
background.alpha = 0.8
result.addSubview(background)
background.pin(to: result)
let imageView = UIImageView() let imageView = UIImageView()
imageView.layer.cornerRadius = 32 imageView.layer.cornerRadius = 32
imageView.layer.masksToBounds = true imageView.layer.masksToBounds = true
imageView.contentMode = .scaleAspectFill imageView.contentMode = .scaleAspectFill
imageView.image = callVC.call.profilePicture imageView.image = callVC.call.profilePicture
background.addSubview(imageView) result.addSubview(imageView)
imageView.set(.width, to: 64) imageView.set(.width, to: 64)
imageView.set(.height, to: 64) imageView.set(.height, to: 64)
imageView.center(in: background) imageView.center(in: result)
return background
return result
} }
private func setUpGestureRecognizers() { private func setUpGestureRecognizers() {
@ -104,7 +142,8 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
makeViewDraggable() makeViewDraggable()
} }
// MARK: Interaction // MARK: - Interaction
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
dismiss() dismiss()
guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } // FIXME: Handle more gracefully guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } // FIXME: Handle more gracefully
@ -113,14 +152,16 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
public func show() { public func show() {
self.alpha = 0.0 self.alpha = 0.0
let window = CurrentAppContext().mainWindow! guard let window: UIWindow = CurrentAppContext().mainWindow else { return }
window.addSubview(self) window.addSubview(self)
left = self.autoPinEdge(toSuperviewEdge: .left) left = self.autoPinEdge(toSuperviewEdge: .left)
left?.isActive = false left?.isActive = false
right = self.autoPinEdge(toSuperviewEdge: .right) right = self.autoPinEdge(toSuperviewEdge: .right, withInset: Values.smallSpacing)
top = self.autoPinEdge(toSuperviewEdge: .top, withInset: topMargin) top = self.autoPinEdge(toSuperviewEdge: .top, withInset: topMargin)
bottom = self.autoPinEdge(toSuperviewEdge: .bottom, withInset: bottomMargin) bottom = self.autoPinEdge(toSuperviewEdge: .bottom, withInset: bottomMargin)
bottom?.isActive = false bottom?.isActive = false
UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: { UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: {
self.alpha = 1.0 self.alpha = 1.0
}, completion: nil) }, completion: nil)
@ -129,17 +170,24 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
public func dismiss() { public func dismiss() {
UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: { UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: {
self.alpha = 0.0 self.alpha = 0.0
}, completion: { _ in }, completion: { [weak self] _ in
self.callVC.call.removeRemoteVideoRenderer(self.remoteVideoView) if let remoteVideoView: RTCVideoRenderer = self?.remoteVideoView {
self.callVC.setupStateChangeCallbacks() self?.callVC.call.removeRemoteVideoRenderer(remoteVideoView)
}
self?.callVC.setupStateChangeCallbacks()
MiniCallView.current = nil MiniCallView.current = nil
self.removeFromSuperview() self?.removeFromSuperview()
}) })
} }
// MARK: RTCVideoViewDelegate // MARK: - RTCVideoViewDelegate
func videoView(_ videoView: RTCVideoRenderer, didChangeVideoSize size: CGSize) { func videoView(_ videoView: RTCVideoRenderer, didChangeVideoSize size: CGSize) {
let newSize = CGSize(width: min(160.0, 160.0 * size.width / size.height), height: min(160.0, 160.0 * size.height / size.width)) let newSize = CGSize(
width: min(160.0, 160.0 * size.width / size.height),
height: min(160.0, 160.0 * size.height / size.width)
)
persistCurrentPosition(newSize: newSize) persistCurrentPosition(newSize: newSize)
self.width?.constant = newSize.width self.width?.constant = newSize.width
self.height?.constant = newSize.height self.height?.constant = newSize.height
@ -148,25 +196,49 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
func persistCurrentPosition(newSize: CGSize) { func persistCurrentPosition(newSize: CGSize) {
let currentCenter = self.center let currentCenter = self.center
if currentCenter.x < self.superview!.width() / 2 { if currentCenter.x < ((self.superview?.width() ?? 0) / 2) {
left?.isActive = true left?.isActive = true
right?.isActive = false right?.isActive = false
} else { }
else {
left?.isActive = false left?.isActive = false
right?.isActive = true right?.isActive = true
} }
let willTouchTop = currentCenter.y < newSize.height / 2 + topMargin let willTouchTop: Bool = (currentCenter.y < ((newSize.height / 2) + topMargin))
let willTouchBottom = currentCenter.y + newSize.height / 2 >= self.superview!.height() let willTouchBottom: Bool = ((currentCenter.y + (newSize.height / 2)) >= (self.superview?.height() ?? 0))
if willTouchBottom { if willTouchBottom {
top?.isActive = false top?.isActive = false
bottom?.isActive = true bottom?.isActive = true
} else { }
let constant = willTouchTop ? topMargin : currentCenter.y - newSize.height / 2 else {
let constant = (willTouchTop ? topMargin : (currentCenter.y - (newSize.height / 2)))
top?.constant = constant top?.constant = constant
top?.isActive = true top?.isActive = true
bottom?.isActive = false bottom?.isActive = false
} }
} }
@objc private func windowSubviewsChanged() {
// Ensure the MiniCallView always stays in front when presenting screens (need to update the
// constraints to match the current values so when the re-layout occurs it doesn't move)
if self.top?.isActive == true {
self.top?.constant = self.frame.minY
}
if self.left?.isActive == true {
self.left?.constant = self.frame.minX
}
if self.right?.isActive == true {
self.right?.constant = (self.frame.maxX - (self.superview?.width() ?? 0))
}
if self.bottom?.isActive == true {
self.bottom?.constant = (self.frame.maxY - (self.superview?.height() ?? 0))
}
self.window?.bringSubviewToFront(self)
}
} }

View File

@ -79,7 +79,7 @@ extension ConversationVC:
return return
} }
requestMicrophonePermissionIfNeeded {} Permissions.requestMicrophonePermissionIfNeeded()
let threadId: String = self.viewModel.threadData.threadId let threadId: String = self.viewModel.threadData.threadId
@ -104,12 +104,34 @@ extension ConversationVC:
} }
@discardableResult func showBlockedModalIfNeeded() -> Bool { @discardableResult func showBlockedModalIfNeeded() -> Bool {
guard self.viewModel.threadData.threadIsBlocked == true else { return false } guard
self.viewModel.threadData.threadVariant == .contact &&
self.viewModel.threadData.threadIsBlocked == true
else { return false }
let blockedModal = BlockedModal(publicKey: viewModel.threadData.threadId) let message = String(
blockedModal.modalPresentationStyle = .overFullScreen format: "modal_blocked_explanation".localized(),
blockedModal.modalTransitionStyle = .crossDissolve self.viewModel.threadData.displayName
present(blockedModal, animated: true, completion: nil) )
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: String(
format: "modal_blocked_title".localized(),
self.viewModel.threadData.displayName
),
attributedExplanation: NSAttributedString(string: message)
.adding(
attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ],
range: (message as NSString).range(of: self.viewModel.threadData.displayName)
),
confirmTitle: "modal_blocked_button_title".localized(),
dismissOnConfirm: false // Custom dismissal logic
) { [weak self] _ in
self?.viewModel.unblockContact()
self?.dismiss(animated: true, completion: nil)
}
)
present(confirmationModal, animated: true, completion: nil)
return true return true
} }
@ -185,7 +207,7 @@ extension ConversationVC:
func handleLibraryButtonTapped() { func handleLibraryButtonTapped() {
let threadId: String = self.viewModel.threadData.threadId let threadId: String = self.viewModel.threadData.threadId
requestLibraryPermissionIfNeeded { [weak self] in Permissions.requestLibraryPermissionIfNeeded { [weak self] in
DispatchQueue.main.async { DispatchQueue.main.async {
let sendMediaNavController = SendMediaNavigationController.showingMediaLibraryFirst( let sendMediaNavController = SendMediaNavigationController.showingMediaLibraryFirst(
threadId: threadId threadId: threadId
@ -198,9 +220,9 @@ extension ConversationVC:
} }
func handleCameraButtonTapped() { func handleCameraButtonTapped() {
guard requestCameraPermissionIfNeeded() else { return } guard Permissions.requestCameraPermissionIfNeeded(presentingViewController: self) else { return }
requestMicrophonePermissionIfNeeded { } Permissions.requestMicrophonePermissionIfNeeded()
if AVAudioSession.sharedInstance().recordPermission != .granted { if AVAudioSession.sharedInstance().recordPermission != .granted {
SNLog("Proceeding without microphone access. Any recorded video will be silent.") SNLog("Proceeding without microphone access. Any recorded video will be silent.")
@ -760,11 +782,30 @@ extension ConversationVC:
// If it's an incoming media message and the thread isn't trusted then show the placeholder view // If it's an incoming media message and the thread isn't trusted then show the placeholder view
if cellViewModel.cellType != .textOnlyMessage && cellViewModel.variant == .standardIncoming && !cellViewModel.threadIsTrusted { if cellViewModel.cellType != .textOnlyMessage && cellViewModel.variant == .standardIncoming && !cellViewModel.threadIsTrusted {
let modal = DownloadAttachmentModal(profile: cellViewModel.profile) let message: String = String(
modal.modalPresentationStyle = .overFullScreen format: "modal_download_attachment_explanation".localized(),
modal.modalTransitionStyle = .crossDissolve cellViewModel.authorName
)
let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
title: String(
format: "modal_download_attachment_title".localized(),
cellViewModel.authorName
),
attributedExplanation: NSAttributedString(string: message)
.adding(
attributes: [ .font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ],
range: (message as NSString).range(of: cellViewModel.authorName)
),
confirmTitle: "modal_download_button_title".localized(),
dismissOnConfirm: false // Custom dismissal logic
) { [weak self] _ in
self?.viewModel.trustContact()
self?.dismiss(animated: true, completion: nil)
}
)
present(modal, animated: true, completion: nil) present(confirmationModal, animated: true, completion: nil)
return return
} }
@ -924,20 +965,20 @@ extension ConversationVC:
guard let url: URL = URL(string: urlString) else { return } guard let url: URL = URL(string: urlString) else { return }
// URLs can be unsafe, so always ask the user whether they want to open one // URLs can be unsafe, so always ask the user whether they want to open one
let alertVC = UIAlertController.init( let alertVC = UIAlertController(
title: "modal_open_url_title".localized(), title: "modal_open_url_title".localized(),
message: String(format: "modal_open_url_explanation".localized(), url.absoluteString), message: String(format: "modal_open_url_explanation".localized(), url.absoluteString),
preferredStyle: .actionSheet preferredStyle: .actionSheet
) )
alertVC.addAction(UIAlertAction.init(title: "modal_open_url_button_title".localized(), style: .default) { [weak self] _ in alertVC.addAction(UIAlertAction(title: "modal_open_url_button_title".localized(), style: .default) { [weak self] _ in
UIApplication.shared.open(url, options: [:], completionHandler: nil) UIApplication.shared.open(url, options: [:], completionHandler: nil)
self?.showInputAccessoryView() self?.showInputAccessoryView()
}) })
alertVC.addAction(UIAlertAction.init(title: "modal_copy_url_button_title".localized(), style: .default) { [weak self] _ in alertVC.addAction(UIAlertAction(title: "modal_copy_url_button_title".localized(), style: .default) { [weak self] _ in
UIPasteboard.general.string = url.absoluteString UIPasteboard.general.string = url.absoluteString
self?.showInputAccessoryView() self?.showInputAccessoryView()
}) })
alertVC.addAction(UIAlertAction.init(title: "cancel".localized(), style: .cancel) { [weak self] _ in alertVC.addAction(UIAlertAction(title: "cancel".localized(), style: .cancel) { [weak self] _ in
self?.showInputAccessoryView() self?.showInputAccessoryView()
}) })
@ -1732,7 +1773,7 @@ extension ConversationVC:
func startVoiceMessageRecording() { func startVoiceMessageRecording() {
// Request permission if needed // Request permission if needed
requestMicrophonePermissionIfNeeded() { [weak self] in Permissions.requestMicrophonePermissionIfNeeded() { [weak self] in
self?.cancelVoiceMessageRecording() self?.cancelVoiceMessageRecording()
} }
@ -1854,100 +1895,6 @@ extension ConversationVC:
Environment.shared?.audioSession.endAudioActivity(recordVoiceMessageActivity) Environment.shared?.audioSession.endAudioActivity(recordVoiceMessageActivity)
} }
// MARK: - Permissions
func requestCameraPermissionIfNeeded() -> Bool {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized: return true
case .denied, .restricted:
let modal = PermissionMissingModal(permission: "camera") { }
modal.modalPresentationStyle = .overFullScreen
modal.modalTransitionStyle = .crossDissolve
present(modal, animated: true, completion: nil)
return false
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video, completionHandler: { _ in })
return false
default: return false
}
}
func requestMicrophonePermissionIfNeeded(onNotGranted: @escaping () -> Void) {
switch AVAudioSession.sharedInstance().recordPermission {
case .granted: break
case .denied:
onNotGranted()
let modal = PermissionMissingModal(permission: "microphone") {
onNotGranted()
}
modal.modalPresentationStyle = .overFullScreen
modal.modalTransitionStyle = .crossDissolve
present(modal, animated: true, completion: nil)
case .undetermined:
onNotGranted()
AVAudioSession.sharedInstance().requestRecordPermission { _ in }
default: break
}
}
func requestLibraryPermissionIfNeeded(onAuthorized: @escaping () -> Void) {
let authorizationStatus: PHAuthorizationStatus
if #available(iOS 14, *) {
authorizationStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
if authorizationStatus == .notDetermined {
// When the user chooses to select photos (which is the .limit status),
// the PHPhotoUI will present the picker view on the top of the front view.
// Since we have the ScreenLockUI showing when we request premissions,
// the picker view will be presented on the top of the ScreenLockUI.
// However, the ScreenLockUI will dismiss with the permission request alert view, so
// the picker view then will dismiss, too. The selection process cannot be finished
// this way. So we add a flag (isRequestingPermission) to prevent the ScreenLockUI
// from showing when we request the photo library permission.
Environment.shared?.isRequestingPermission = true
let appMode = AppModeManager.shared.currentAppMode
// FIXME: Rather than setting the app mode to light and then to dark again once we're done,
// it'd be better to just customize the appearance of the image picker. There doesn't currently
// appear to be a good way to do so though...
AppModeManager.shared.setCurrentAppMode(to: .light)
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
DispatchQueue.main.async {
AppModeManager.shared.setCurrentAppMode(to: appMode)
}
Environment.shared?.isRequestingPermission = false
if [ PHAuthorizationStatus.authorized, PHAuthorizationStatus.limited ].contains(status) {
onAuthorized()
}
}
}
} else {
authorizationStatus = PHPhotoLibrary.authorizationStatus()
if authorizationStatus == .notDetermined {
PHPhotoLibrary.requestAuthorization { status in
if status == .authorized {
onAuthorized()
}
}
}
}
switch authorizationStatus {
case .authorized, .limited:
onAuthorized()
case .denied, .restricted:
let modal = PermissionMissingModal(permission: "library") { }
modal.modalPresentationStyle = .overFullScreen
modal.modalTransitionStyle = .crossDissolve
present(modal, animated: true, completion: nil)
default: return
}
}
// MARK: - Data Extraction Notifications // MARK: - Data Extraction Notifications
@objc func sendScreenshotNotification() { @objc func sendScreenshotNotification() {

View File

@ -186,7 +186,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
lazy var blockedBanner: InfoBanner = { lazy var blockedBanner: InfoBanner = {
let result: InfoBanner = InfoBanner( let result: InfoBanner = InfoBanner(
message: self.viewModel.blockedBannerMessage, message: self.viewModel.blockedBannerMessage,
backgroundColor: Colors.destructive backgroundColor: .danger
) )
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(unblock)) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(unblock))
result.addGestureRecognizer(tapGestureRecognizer) result.addGestureRecognizer(tapGestureRecognizer)

View File

@ -509,6 +509,53 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
} }
} }
public func trustContact() {
guard self.threadData.threadVariant == .contact else { return }
let threadId: String = self.threadId
Storage.shared.writeAsync { db in
try Contact
.filter(id: threadId)
.updateAll(db, Contact.Columns.isTrusted.set(to: true))
// Start downloading any pending attachments for this contact (UI will automatically be
// updated due to the database observation)
try Attachment
.stateInfo(authorId: threadId, state: .pendingDownload)
.fetchAll(db)
.forEach { attachmentDownloadInfo in
JobRunner.add(
db,
job: Job(
variant: .attachmentDownload,
threadId: threadId,
interactionId: attachmentDownloadInfo.interactionId,
details: AttachmentDownloadJob.Details(
attachmentId: attachmentDownloadInfo.attachmentId
)
)
)
}
}
}
public func unblockContact() {
guard self.threadData.threadVariant == .contact else { return }
let threadId: String = self.threadId
Storage.shared.writeAsync { db in
try Contact
.filter(id: threadId)
.updateAll(db, Contact.Columns.isBlocked.set(to: false))
try MessageSender
.syncConfiguration(db, forceSyncNow: true)
.retainUntilComplete()
}
}
// MARK: - Audio Playback // MARK: - Audio Playback
public struct PlaybackInfo { public struct PlaybackInfo {

View File

@ -1,93 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import GRDB
import SessionUIKit
import SessionUtilitiesKit
import SessionMessagingKit
final class BlockedModal: Modal {
private let publicKey: String
// MARK: Lifecycle
init(publicKey: String) {
self.publicKey = publicKey
super.init(nibName: nil, bundle: nil)
}
override init(nibName: String?, bundle: Bundle?) {
preconditionFailure("Use init(publicKey:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(publicKey:) instead.")
}
override func populateContentView() {
// Name
let name = Profile.displayName(id: publicKey)
// Title
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
titleLabel.text = String(format: NSLocalizedString("modal_blocked_title", comment: ""), name)
titleLabel.textAlignment = .center
// Message
let messageLabel = UILabel()
messageLabel.textColor = Colors.text
messageLabel.font = .systemFont(ofSize: Values.smallFontSize)
let message = String(format: NSLocalizedString("modal_blocked_explanation", comment: ""), name)
let attributedMessage = NSMutableAttributedString(string: message)
attributedMessage.addAttributes([ .font : UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], range: (message as NSString).range(of: name))
messageLabel.attributedText = attributedMessage
messageLabel.numberOfLines = 0
messageLabel.lineBreakMode = .byWordWrapping
messageLabel.textAlignment = .center
// Unblock button
let unblockButton = UIButton()
unblockButton.set(.height, to: Values.mediumButtonHeight)
unblockButton.layer.cornerRadius = Modal.buttonCornerRadius
unblockButton.backgroundColor = Colors.buttonBackground
unblockButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
unblockButton.setTitleColor(Colors.text, for: UIControl.State.normal)
unblockButton.setTitle(NSLocalizedString("modal_blocked_button_title", comment: ""), for: UIControl.State.normal)
unblockButton.addTarget(self, action: #selector(unblock), for: UIControl.Event.touchUpInside)
// Button stack view
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, unblockButton ])
buttonStackView.axis = .horizontal
buttonStackView.spacing = Values.mediumSpacing
buttonStackView.distribution = .fillEqually
// Content stack view
let contentStackView = UIStackView(arrangedSubviews: [ titleLabel, messageLabel ])
contentStackView.axis = .vertical
contentStackView.spacing = Values.largeSpacing
// Main stack view
let spacing = Values.largeSpacing - Values.smallFontSize / 2
let mainStackView = UIStackView(arrangedSubviews: [ contentStackView, buttonStackView ])
mainStackView.axis = .vertical
mainStackView.spacing = spacing
contentView.addSubview(mainStackView)
mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.largeSpacing)
contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: spacing)
}
// MARK: - Interaction
@objc private func unblock() {
let publicKey: String = self.publicKey
Storage.shared.writeAsync { db in
try Contact
.filter(id: publicKey)
.updateAll(db, Contact.Columns.isBlocked.set(to: false))
try MessageSender
.syncConfiguration(db, forceSyncNow: true)
.retainUntilComplete()
}
presentingViewController?.dismiss(animated: true, completion: nil)
}
}

View File

@ -1,83 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionMessagingKit
@objc
final class CallModal: Modal {
private let onCallEnabled: () -> Void
// MARK: - Lifecycle
@objc
init(onCallEnabled: @escaping () -> Void) {
self.onCallEnabled = onCallEnabled
super.init(nibName: nil, bundle: nil)
self.modalPresentationStyle = .overFullScreen
self.modalTransitionStyle = .crossDissolve
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(onCallEnabled:) instead.")
}
override init(nibName: String?, bundle: Bundle?) {
preconditionFailure("Use init(onCallEnabled:) instead.")
}
override func populateContentView() {
// Title
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.largeFontSize)
titleLabel.text = NSLocalizedString("modal_call_title", comment: "")
titleLabel.textAlignment = .center
// Message
let messageLabel = UILabel()
messageLabel.textColor = Colors.text
messageLabel.font = .systemFont(ofSize: Values.smallFontSize)
messageLabel.text = "modal_call_explanation".localized()
messageLabel.numberOfLines = 0
messageLabel.lineBreakMode = .byWordWrapping
messageLabel.textAlignment = .center
// Enable button
let enableButton = UIButton()
enableButton.set(.height, to: Values.mediumButtonHeight)
enableButton.layer.cornerRadius = Modal.buttonCornerRadius
enableButton.backgroundColor = Colors.buttonBackground
enableButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
enableButton.setTitleColor(Colors.text, for: UIControl.State.normal)
enableButton.setTitle(NSLocalizedString("modal_link_previews_button_title", comment: ""), for: UIControl.State.normal)
enableButton.addTarget(self, action: #selector(enable), for: UIControl.Event.touchUpInside)
// Button stack view
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, enableButton ])
buttonStackView.axis = .horizontal
buttonStackView.spacing = Values.mediumSpacing
buttonStackView.distribution = .fillEqually
// Main stack view
let mainStackView = UIStackView(arrangedSubviews: [ titleLabel, messageLabel, buttonStackView ])
mainStackView.axis = .vertical
mainStackView.spacing = Values.largeSpacing
contentView.addSubview(mainStackView)
mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.largeSpacing)
contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: Values.largeSpacing)
}
// MARK: - Interaction
@objc private func enable() {
Storage.shared.writeAsync { db in db[.areCallsEnabled] = true }
presentingViewController?.dismiss(animated: true, completion: nil)
onCallEnabled()
}
}

View File

@ -17,8 +17,8 @@ final class ConversationTitleView: UIView {
private lazy var titleLabel: UILabel = { private lazy var titleLabel: UILabel = {
let result: UILabel = UILabel() let result: UILabel = UILabel()
result.textColor = Colors.text
result.font = .boldSystemFont(ofSize: Values.mediumFontSize) result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
result.themeTextColor = .textPrimary
result.lineBreakMode = .byTruncatingTail result.lineBreakMode = .byTruncatingTail
return result return result
@ -26,8 +26,8 @@ final class ConversationTitleView: UIView {
private lazy var subtitleLabel: UILabel = { private lazy var subtitleLabel: UILabel = {
let result: UILabel = UILabel() let result: UILabel = UILabel()
result.textColor = Colors.text
result.font = .systemFont(ofSize: 13) result.font = .systemFont(ofSize: 13)
result.themeTextColor = .textPrimary
result.lineBreakMode = .byTruncatingTail result.lineBreakMode = .byTruncatingTail
return result return result
@ -95,23 +95,37 @@ final class ConversationTitleView: UIView {
return return
} }
// Generate the subtitle let shouldHaveSubtitle: Bool = (
let subtitle: NSAttributedString? = { Date().timeIntervalSince1970 <= (mutedUntilTimestamp ?? 0) ||
onlyNotifyForMentions ||
userCount != nil
)
self.titleLabel.text = name
self.titleLabel.font = .boldSystemFont(
ofSize: (shouldHaveSubtitle ?
Values.mediumFontSize :
Values.veryLargeFontSize
)
)
ThemeManager.onThemeChange(observer: self.subtitleLabel) { [weak subtitleLabel] theme, _ in
guard let textPrimary: UIColor = theme.colors[.textPrimary] else { return }
//subtitleLabel?.attributedText = subtitle
guard Date().timeIntervalSince1970 > (mutedUntilTimestamp ?? 0) else { guard Date().timeIntervalSince1970 > (mutedUntilTimestamp ?? 0) else {
return NSAttributedString( subtitleLabel?.attributedText = NSAttributedString(
string: "\u{e067} ", string: "\u{e067} ",
attributes: [ attributes: [
.font: UIFont.ows_elegantIconsFont(10), .font: UIFont.ows_elegantIconsFont(10),
.foregroundColor: Colors.text .foregroundColor: textPrimary
] ]
) )
.appending(string: "Muted") .appending(string: "Muted")
return
} }
guard !onlyNotifyForMentions else { guard !onlyNotifyForMentions else {
// FIXME: This is going to have issues when swapping between light/dark mode
let imageAttachment = NSTextAttachment() let imageAttachment = NSTextAttachment()
let color: UIColor = (isDarkMode ? .white : .black) imageAttachment.image = UIImage(named: "NotifyMentions.png")?.asTintedImage(color: textPrimary)
imageAttachment.image = UIImage(named: "NotifyMentions.png")?.asTintedImage(color: color)
imageAttachment.bounds = CGRect( imageAttachment.bounds = CGRect(
x: 0, x: 0,
y: -2, y: -2,
@ -119,23 +133,17 @@ final class ConversationTitleView: UIView {
height: Values.smallFontSize height: Values.smallFontSize
) )
return NSAttributedString(attachment: imageAttachment) subtitleLabel?.attributedText = NSAttributedString(attachment: imageAttachment)
.appending(string: " ") .appending(string: " ")
.appending(string: "view_conversation_title_notify_for_mentions_only".localized()) .appending(string: "view_conversation_title_notify_for_mentions_only".localized())
return
} }
guard let userCount: Int = userCount else { return nil } guard let userCount: Int = userCount else { return }
return NSAttributedString(string: "\(userCount) member\(userCount == 1 ? "" : "s")") subtitleLabel?.attributedText = NSAttributedString(
}() string: "\(userCount) member\(userCount == 1 ? "" : "s")"
self.titleLabel.text = name
self.titleLabel.font = .boldSystemFont(
ofSize: (subtitle != nil ?
Values.mediumFontSize :
Values.veryLargeFontSize
) )
) }
self.subtitleLabel.attributedText = subtitle
// Contact threads also have the call button to compensate for // Contact threads also have the call button to compensate for
let shouldShowCallButton: Bool = ( let shouldShowCallButton: Bool = (

View File

@ -1,120 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import GRDB
import SessionUIKit
import SessionUtilitiesKit
import SessionMessagingKit
final class DownloadAttachmentModal: Modal {
private let profile: Profile?
// MARK: - Lifecycle
init(profile: Profile?) {
self.profile = profile
super.init(nibName: nil, bundle: nil)
}
override init(nibName: String?, bundle: Bundle?) {
preconditionFailure("Use init(viewItem:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(viewItem:) instead.")
}
override func populateContentView() {
guard let profile: Profile = profile else { return }
// Name
let name: String = profile.displayName()
// Title
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
titleLabel.text = String(format: NSLocalizedString("modal_download_attachment_title", comment: ""), name)
titleLabel.textAlignment = .center
// Message
let messageLabel = UILabel()
messageLabel.textColor = Colors.text
messageLabel.font = .systemFont(ofSize: Values.smallFontSize)
let message = String(format: NSLocalizedString("modal_download_attachment_explanation", comment: ""), name)
let attributedMessage = NSMutableAttributedString(string: message)
attributedMessage.addAttributes(
[.font: UIFont.boldSystemFont(ofSize: Values.smallFontSize) ],
range: (message as NSString).range(of: name)
)
messageLabel.attributedText = attributedMessage
messageLabel.numberOfLines = 0
messageLabel.lineBreakMode = .byWordWrapping
messageLabel.textAlignment = .center
// Download button
let downloadButton = UIButton()
downloadButton.set(.height, to: Values.mediumButtonHeight)
downloadButton.layer.cornerRadius = Modal.buttonCornerRadius
downloadButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
downloadButton.setTitleColor(Colors.text, for: UIControl.State.normal)
downloadButton.setTitle(NSLocalizedString("modal_download_button_title", comment: ""), for: UIControl.State.normal)
downloadButton.addTarget(self, action: #selector(trust), for: UIControl.Event.touchUpInside)
// Button stack view
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, downloadButton ])
buttonStackView.axis = .horizontal
buttonStackView.spacing = Values.mediumSpacing
buttonStackView.distribution = .fillEqually
// Content stack view
let contentStackView = UIStackView(arrangedSubviews: [ titleLabel, messageLabel ])
contentStackView.axis = .vertical
contentStackView.spacing = Values.largeSpacing
// Main stack view
let spacing = Values.largeSpacing - Values.smallFontSize / 2
let mainStackView = UIStackView(arrangedSubviews: [ contentStackView, buttonStackView ])
mainStackView.axis = .vertical
mainStackView.spacing = spacing
contentView.addSubview(mainStackView)
mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.largeSpacing)
contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: spacing)
}
// MARK: - Interaction
@objc private func trust() {
guard let profileId: String = profile?.id else { return }
Storage.shared.writeAsync { db in
try Contact
.filter(id: profileId)
.updateAll(db, Contact.Columns.isTrusted.set(to: true))
// Start downloading any pending attachments for this contact (UI will automatically be
// updated due to the database observation)
try Attachment
.stateInfo(authorId: profileId, state: .pendingDownload)
.fetchAll(db)
.forEach { attachmentDownloadInfo in
JobRunner.add(
db,
job: Job(
variant: .attachmentDownload,
threadId: profileId,
interactionId: attachmentDownloadInfo.interactionId,
details: AttachmentDownloadJob.Details(
attachmentId: attachmentDownloadInfo.attachmentId
)
)
)
}
}
presentingViewController?.dismiss(animated: true, completion: nil)
}
}

View File

@ -1,13 +1,13 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
final class InfoBanner : UIView { import UIKit
private let message: String import SessionUIKit
private let snBackgroundColor: UIColor
final class InfoBanner: UIView {
init(message: String, backgroundColor: UIColor) { init(message: String, backgroundColor: ThemeValue) {
self.message = message
self.snBackgroundColor = backgroundColor
super.init(frame: CGRect.zero) super.init(frame: CGRect.zero)
setUpViewHierarchy()
setUpViewHierarchy(message: message, backgroundColor: backgroundColor)
} }
override init(frame: CGRect) { override init(frame: CGRect) {
@ -18,16 +18,18 @@ final class InfoBanner : UIView {
preconditionFailure("Use init(coder:) instead.") preconditionFailure("Use init(coder:) instead.")
} }
private func setUpViewHierarchy() { private func setUpViewHierarchy(message: String, backgroundColor: ThemeValue) {
backgroundColor = snBackgroundColor themeBackgroundColor = backgroundColor
let label = UILabel()
label.text = message let label: UILabel = UILabel()
label.font = .boldSystemFont(ofSize: Values.smallFontSize) label.font = .boldSystemFont(ofSize: Values.smallFontSize)
label.textColor = .white label.text = message
label.numberOfLines = 0 label.themeTextColor = .textPrimary
label.textAlignment = .center label.textAlignment = .center
label.lineBreakMode = .byWordWrapping label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
addSubview(label) addSubview(label)
label.pin(to: self, withInset: Values.mediumSpacing) label.pin(to: self, withInset: Values.mediumSpacing)
} }
} }

View File

@ -15,10 +15,10 @@ final class JoinOpenGroupModal: Modal {
self.name = (name ?? "Open Group") self.name = (name ?? "Open Group")
self.url = url self.url = url
super.init(nibName: nil, bundle: nil) super.init()
} }
override init(nibName: String?, bundle: Bundle?) { override init(afterClosed: (() -> ())? = nil) {
preconditionFailure("Use init(name:url:) instead.") preconditionFailure("Use init(name:url:) instead.")
} }

View File

@ -1,80 +0,0 @@
final class PermissionMissingModal : Modal {
private let permission: String
private let onCancel: () -> Void
// MARK: Lifecycle
init(permission: String, onCancel: @escaping () -> Void) {
self.permission = permission
self.onCancel = onCancel
super.init(nibName: nil, bundle: nil)
}
override init(nibName: String?, bundle: Bundle?) {
preconditionFailure("Use init(permission:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(permission:) instead.")
}
override func populateContentView() {
// Title
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
titleLabel.text = "Session"
titleLabel.textAlignment = .center
// Message
let messageLabel = UILabel()
messageLabel.textColor = Colors.text
messageLabel.font = .systemFont(ofSize: Values.smallFontSize)
let message = "Session needs \(permission) access to continue. You can enable access in the iOS settings."
let attributedMessage = NSMutableAttributedString(string: message)
attributedMessage.addAttributes([ .font : UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], range: (message as NSString).range(of: permission))
messageLabel.attributedText = attributedMessage
messageLabel.numberOfLines = 0
messageLabel.lineBreakMode = .byWordWrapping
messageLabel.textAlignment = .center
// Settings button
let settingsButton = UIButton()
settingsButton.set(.height, to: Values.mediumButtonHeight)
settingsButton.layer.cornerRadius = Modal.buttonCornerRadius
settingsButton.backgroundColor = Colors.buttonBackground
settingsButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
settingsButton.setTitleColor(Colors.text, for: UIControl.State.normal)
settingsButton.setTitle("Settings", for: UIControl.State.normal)
settingsButton.addTarget(self, action: #selector(goToSettings), for: UIControl.Event.touchUpInside)
// Button stack view
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, settingsButton ])
buttonStackView.axis = .horizontal
buttonStackView.spacing = Values.mediumSpacing
buttonStackView.distribution = .fillEqually
// Content stack view
let contentStackView = UIStackView(arrangedSubviews: [ titleLabel, messageLabel ])
contentStackView.axis = .vertical
contentStackView.spacing = Values.largeSpacing
// Main stack view
let spacing = Values.largeSpacing - Values.smallFontSize / 2
let mainStackView = UIStackView(arrangedSubviews: [ contentStackView, buttonStackView ])
mainStackView.axis = .vertical
mainStackView.spacing = spacing
contentView.addSubview(mainStackView)
mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.largeSpacing)
contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: spacing)
}
// MARK: Interaction
@objc private func goToSettings() {
presentingViewController?.dismiss(animated: true, completion: {
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
})
}
override func close() {
super.close()
onCancel()
}
}

View File

@ -1,86 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
final class URLModal: Modal {
private let url: URL
// MARK: - Lifecycle
init(url: URL) {
self.url = url
super.init(nibName: nil, bundle: nil)
}
override init(nibName: String?, bundle: Bundle?) {
preconditionFailure("Use init(url:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(url:) instead.")
}
override func populateContentView() {
// Title
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
titleLabel.text = NSLocalizedString("modal_open_url_title", comment: "")
titleLabel.textAlignment = .center
// Message
let messageLabel = UILabel()
messageLabel.textColor = Colors.text
messageLabel.font = .systemFont(ofSize: Values.smallFontSize)
let message = String(format: NSLocalizedString("modal_open_url_explanation", comment: ""), url.absoluteString)
let attributedMessage = NSMutableAttributedString(string: message)
attributedMessage.addAttributes([ .font : UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], range: (message as NSString).range(of: url.absoluteString))
messageLabel.attributedText = attributedMessage
messageLabel.numberOfLines = 0
messageLabel.lineBreakMode = .byWordWrapping
messageLabel.textAlignment = .center
// Open button
let openButton = UIButton()
openButton.set(.height, to: Values.mediumButtonHeight)
openButton.layer.cornerRadius = Modal.buttonCornerRadius
openButton.backgroundColor = Colors.buttonBackground
openButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
openButton.setTitleColor(Colors.text, for: UIControl.State.normal)
openButton.setTitle(NSLocalizedString("modal_open_url_button_title", comment: ""), for: UIControl.State.normal)
openButton.addTarget(self, action: #selector(openUrl), for: UIControl.Event.touchUpInside)
// Button stack view
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, openButton ])
buttonStackView.axis = .horizontal
buttonStackView.spacing = Values.mediumSpacing
buttonStackView.distribution = .fillEqually
// Content stack view
let contentStackView = UIStackView(arrangedSubviews: [ titleLabel, messageLabel ])
contentStackView.axis = .vertical
contentStackView.spacing = Values.largeSpacing
// Main stack view
let spacing = Values.largeSpacing - Values.smallFontSize / 2
let mainStackView = UIStackView(arrangedSubviews: [ contentStackView, buttonStackView ])
mainStackView.axis = .vertical
mainStackView.spacing = spacing
contentView.addSubview(mainStackView)
mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.largeSpacing)
contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: spacing)
}
// MARK: - Interaction
@objc private func openUrl() {
let url = self.url
presentingViewController?.dismiss(animated: true, completion: {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
})
}
}

View File

@ -547,47 +547,31 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
style: .destructive, style: .destructive,
title: "TXT_DELETE_TITLE".localized() title: "TXT_DELETE_TITLE".localized()
) { [weak self] _, _, completionHandler in ) { [weak self] _, _, completionHandler in
let message = (threadViewModel.currentUserIsClosedGroupAdmin == true ? let confirmationModal: ConfirmationModal = ConfirmationModal(
"admin_group_leave_warning".localized() : info: ConfirmationModal.Info(
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE".localized() title: "CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE".localized(),
explanation: (threadViewModel.currentUserIsClosedGroupAdmin == true ?
"admin_group_leave_warning".localized() :
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE".localized()
),
confirmTitle: "TXT_DELETE_TITLE".localized(),
confirmStyle: .danger,
cancelStyle: .textPrimary,
dismissOnConfirm: true,
onConfirm: { [weak self] _ in
self?.viewModel.delete(
threadId: threadViewModel.threadId,
threadVariant: threadViewModel.threadVariant
)
self?.dismiss(animated: true, completion: nil)
completionHandler(true)
},
afterClosed: { completionHandler(false) }
)
) )
let alert = UIAlertController( self?.present(confirmationModal, animated: true, completion: nil)
title: "CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE".localized(),
message: message,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(
title: "TXT_DELETE_TITLE".localized(),
style: .destructive
) { _ in
Storage.shared.writeAsync { db in
switch threadViewModel.threadVariant {
case .closedGroup:
try MessageSender
.leave(db, groupPublicKey: threadViewModel.threadId)
.retainUntilComplete()
case .openGroup:
OpenGroupManager.shared.delete(db, openGroupId: threadViewModel.threadId)
default: break
}
_ = try SessionThread
.filter(id: threadViewModel.threadId)
.deleteAll(db)
}
completionHandler(true)
})
alert.addAction(UIAlertAction(
title: "TXT_CANCEL_TITLE".localized(),
style: .default
) { _ in
completionHandler(false)
})
self?.present(alert, animated: true, completion: nil)
} }
delete.themeBackgroundColor = .conversationButton_swipeDestructive delete.themeBackgroundColor = .conversationButton_swipeDestructive

View File

@ -310,4 +310,26 @@ public class HomeViewModel {
public func updateThreadData(_ updatedData: [SectionModel]) { public func updateThreadData(_ updatedData: [SectionModel]) {
self.threadData = updatedData self.threadData = updatedData
} }
// MARK: - Functions
public func delete(threadId: String, threadVariant: SessionThread.Variant) {
Storage.shared.writeAsync { db in
switch threadVariant {
case .closedGroup:
try MessageSender
.leave(db, groupPublicKey: threadId)
.retainUntilComplete()
case .openGroup:
OpenGroupManager.shared.delete(db, openGroupId: threadId)
default: break
}
_ = try SessionThread
.filter(id: threadId)
.deleteAll(db)
}
}
} }

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Ihr Wiederherstellungssatz"; "modal_seed_title" = "Ihr Wiederherstellungssatz";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Your Recovery Phrase"; "modal_seed_title" = "Your Recovery Phrase";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Tu frase de recuperación"; "modal_seed_title" = "Tu frase de recuperación";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "عبارت بازیابی شما"; "modal_seed_title" = "عبارت بازیابی شما";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Palatusvirkkeesi"; "modal_seed_title" = "Palatusvirkkeesi";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Votre phrase de récupération"; "modal_seed_title" = "Votre phrase de récupération";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Your Recovery Phrase"; "modal_seed_title" = "Your Recovery Phrase";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Fraza za oporavak"; "modal_seed_title" = "Fraza za oporavak";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Kata pemulihan anda"; "modal_seed_title" = "Kata pemulihan anda";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Frase di recupero"; "modal_seed_title" = "Frase di recupero";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "あなたのリカバリーフレーズ"; "modal_seed_title" = "あなたのリカバリーフレーズ";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Uw Herstel Zin"; "modal_seed_title" = "Uw Herstel Zin";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Twoja fraza odzyskiwania"; "modal_seed_title" = "Twoja fraza odzyskiwania";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Sua frase de recuperação"; "modal_seed_title" = "Sua frase de recuperação";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Ваша секретная фраза"; "modal_seed_title" = "Ваша секретная фраза";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Your Recovery Phrase"; "modal_seed_title" = "Your Recovery Phrase";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Vaša fráza pre obnovenie"; "modal_seed_title" = "Vaša fráza pre obnovenie";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Din Återställningsfras"; "modal_seed_title" = "Din Återställningsfras";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "วลีกู้คืนของคุณ"; "modal_seed_title" = "วลีกู้คืนของคุณ";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "Cụm từ khôi phục của bạn"; "modal_seed_title" = "Cụm từ khôi phục của bạn";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "您的回復用字句"; "modal_seed_title" = "您的回復用字句";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -771,3 +771,8 @@
"modal_clear_all_data_confirm" = "Clear"; "modal_clear_all_data_confirm" = "Clear";
"modal_seed_title" = "您的恢复口令"; "modal_seed_title" = "您的恢复口令";
"modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device."; "modal_seed_explanation" = "You can use your recovery phrase to restore your account or link a device.";
"modal_permission_explanation" = "Session needs %@ access to continue. You can enable access in the iOS settings.";
"modal_permission_settings_title" = "Settings";
"modal_permission_camera" = "camera";
"modal_permission_microphone" = "microphone";
"modal_permission_library" = "library";

View File

@ -145,7 +145,7 @@ class PrivacySettingsViewModel: SettingsTableViewModel<PrivacySettingsViewModel.
stateToShow: .whenDisabled, stateToShow: .whenDisabled,
confirmTitle: "continue_2".localized(), confirmTitle: "continue_2".localized(),
confirmStyle: .textPrimary confirmStyle: .textPrimary
) { _ in requestMicrophonePermissionIfNeeded() } ) { _ in Permissions.requestMicrophonePermissionIfNeeded() }
) )
) )
] ]

View File

@ -21,6 +21,7 @@ public class ConfirmationModal: Modal {
let title: String let title: String
let explanation: String? let explanation: String?
let attributedExplanation: NSAttributedString?
let stateToShow: State let stateToShow: State
let confirmTitle: String? let confirmTitle: String?
let confirmStyle: ThemeValue let confirmStyle: ThemeValue
@ -28,22 +29,26 @@ public class ConfirmationModal: Modal {
let cancelStyle: ThemeValue let cancelStyle: ThemeValue
let dismissOnConfirm: Bool let dismissOnConfirm: Bool
let onConfirm: ((UIViewController) -> ())? let onConfirm: ((UIViewController) -> ())?
let afterClosed: (() -> ())?
// MARK: - Initialization // MARK: - Initialization
init( init(
title: String, title: String,
explanation: String? = nil, explanation: String? = nil,
attributedExplanation: NSAttributedString? = nil,
stateToShow: State = .always, stateToShow: State = .always,
confirmTitle: String? = nil, confirmTitle: String? = nil,
confirmStyle: ThemeValue = .textPrimary, confirmStyle: ThemeValue = .textPrimary,
cancelTitle: String = "TXT_CANCEL_TITLE".localized(), cancelTitle: String = "TXT_CANCEL_TITLE".localized(),
cancelStyle: ThemeValue = .danger, cancelStyle: ThemeValue = .danger,
dismissOnConfirm: Bool = true, dismissOnConfirm: Bool = true,
onConfirm: ((UIViewController) -> ())? = nil onConfirm: ((UIViewController) -> ())? = nil,
afterClosed: (() -> ())? = nil
) { ) {
self.title = title self.title = title
self.explanation = explanation self.explanation = explanation
self.attributedExplanation = attributedExplanation
self.stateToShow = stateToShow self.stateToShow = stateToShow
self.confirmTitle = confirmTitle self.confirmTitle = confirmTitle
self.confirmStyle = confirmStyle self.confirmStyle = confirmStyle
@ -51,11 +56,15 @@ public class ConfirmationModal: Modal {
self.cancelStyle = cancelStyle self.cancelStyle = cancelStyle
self.dismissOnConfirm = dismissOnConfirm self.dismissOnConfirm = dismissOnConfirm
self.onConfirm = onConfirm self.onConfirm = onConfirm
self.afterClosed = afterClosed
} }
// MARK: - Mutation // MARK: - Mutation
public func with(onConfirm: ((UIViewController) -> ())? = nil) -> Info { public func with(
onConfirm: ((UIViewController) -> ())? = nil,
afterClosed: (() -> ())? = nil
) -> Info {
return Info( return Info(
title: self.title, title: self.title,
explanation: self.explanation, explanation: self.explanation,
@ -65,7 +74,8 @@ public class ConfirmationModal: Modal {
cancelTitle: self.cancelTitle, cancelTitle: self.cancelTitle,
cancelStyle: self.cancelStyle, cancelStyle: self.cancelStyle,
dismissOnConfirm: self.dismissOnConfirm, dismissOnConfirm: self.dismissOnConfirm,
onConfirm: (onConfirm ?? self.onConfirm) onConfirm: (onConfirm ?? self.onConfirm),
afterClosed: (afterClosed ?? self.afterClosed)
) )
} }
@ -75,6 +85,7 @@ public class ConfirmationModal: Modal {
return ( return (
lhs.title == rhs.title && lhs.title == rhs.title &&
lhs.explanation == rhs.explanation && lhs.explanation == rhs.explanation &&
lhs.attributedExplanation == rhs.attributedExplanation &&
lhs.stateToShow == rhs.stateToShow && lhs.stateToShow == rhs.stateToShow &&
lhs.confirmTitle == rhs.confirmTitle && lhs.confirmTitle == rhs.confirmTitle &&
lhs.confirmStyle == rhs.confirmStyle && lhs.confirmStyle == rhs.confirmStyle &&
@ -87,6 +98,7 @@ public class ConfirmationModal: Modal {
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
title.hash(into: &hasher) title.hash(into: &hasher)
explanation.hash(into: &hasher) explanation.hash(into: &hasher)
attributedExplanation.hash(into: &hasher)
stateToShow.hash(into: &hasher) stateToShow.hash(into: &hasher)
confirmTitle.hash(into: &hasher) confirmTitle.hash(into: &hasher)
confirmStyle.hash(into: &hasher) confirmStyle.hash(into: &hasher)
@ -174,15 +186,28 @@ public class ConfirmationModal: Modal {
viewController.dismiss(animated: true) viewController.dismiss(animated: true)
} }
super.init(nibName: nil, bundle: nil) super.init(afterClosed: info.afterClosed)
self.modalPresentationStyle = .overFullScreen self.modalPresentationStyle = .overFullScreen
self.modalTransitionStyle = .crossDissolve self.modalTransitionStyle = .crossDissolve
// Set the content based on the provided info // Set the content based on the provided info
titleLabel.text = info.title titleLabel.text = info.title
explanationLabel.text = info.explanation
explanationLabel.isHidden = (info.explanation == nil) // Note: We should only set the appropriate explanation/attributedExplanation value (as
// setting both when one is null can result in the other being removed)
if let explanation: String = info.explanation {
explanationLabel.text = explanation
}
if let attributedExplanation: NSAttributedString = info.attributedExplanation {
explanationLabel.attributedText = attributedExplanation
}
explanationLabel.isHidden = (
info.explanation == nil &&
info.attributedExplanation == nil
)
confirmButton.setTitle(info.confirmTitle, for: .normal) confirmButton.setTitle(info.confirmTitle, for: .normal)
confirmButton.setThemeTitleColor(info.confirmStyle, for: .normal) confirmButton.setThemeTitleColor(info.confirmStyle, for: .normal)
confirmButton.isHidden = (info.confirmTitle == nil) confirmButton.isHidden = (info.confirmTitle == nil)

View File

@ -6,6 +6,8 @@ import SessionUIKit
public class Modal: BaseVC, UIGestureRecognizerDelegate { public class Modal: BaseVC, UIGestureRecognizerDelegate {
private static let cornerRadius: CGFloat = 11 private static let cornerRadius: CGFloat = 11
private let afterClosed: (() -> ())?
// MARK: - Components // MARK: - Components
lazy var dimmingView: UIView = { lazy var dimmingView: UIView = {
@ -52,6 +54,16 @@ public class Modal: BaseVC, UIGestureRecognizerDelegate {
// MARK: - Lifecycle // MARK: - Lifecycle
public init(afterClosed: (() -> ())? = nil) {
self.afterClosed = afterClosed
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("Use init(afterClosed:) instead")
}
public override func viewDidLoad() { public override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -120,7 +132,9 @@ public class Modal: BaseVC, UIGestureRecognizerDelegate {
targetViewController = targetViewController?.presentingViewController targetViewController = targetViewController?.presentingViewController
} }
targetViewController?.presentingViewController?.dismiss(animated: true, completion: nil) targetViewController?.presentingViewController?.dismiss(animated: true) { [weak self] in
self?.afterClosed?()
}
} }
// MARK: - UIGestureRecognizerDelegate // MARK: - UIGestureRecognizerDelegate

View File

@ -5,96 +5,138 @@ import Photos
import PhotosUI import PhotosUI
import SessionUtilitiesKit import SessionUtilitiesKit
public func requestCameraPermissionIfNeeded() -> Bool { public enum Permissions {
switch AVCaptureDevice.authorizationStatus(for: .video) { public static func requestCameraPermissionIfNeeded(
case .authorized: return true presentingViewController: UIViewController? = nil
case .denied, .restricted: ) -> Bool {
let modal = PermissionMissingModal(permission: "camera") { } switch AVCaptureDevice.authorizationStatus(for: .video) {
modal.modalPresentationStyle = .overFullScreen case .authorized: return true
modal.modalTransitionStyle = .crossDissolve case .denied, .restricted:
guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } guard let presentingViewController: UIViewController = (presentingViewController ?? CurrentAppContext().frontmostViewController()) else { return false }
presentingVC.present(modal, animated: true, completion: nil)
return false let confirmationModal: ConfirmationModal = ConfirmationModal(
info: ConfirmationModal.Info(
case .notDetermined: title: "Session",
AVCaptureDevice.requestAccess(for: .video, completionHandler: { _ in }) explanation: String(
return false format: "modal_permission_explanation".localized(),
"modal_permission_camera".localized()
default: return false ),
confirmTitle: "modal_permission_settings_title".localized(),
dismissOnConfirm: false
) { [weak presentingViewController] _ in
presentingViewController?.dismiss(animated: true, completion: {
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
})
}
)
presentingViewController.present(confirmationModal, animated: true, completion: nil)
return false
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video, completionHandler: { _ in })
return false
default: return false
}
} }
}
public func requestMicrophonePermissionIfNeeded(onNotGranted: (() -> Void)? = nil) { public static func requestMicrophonePermissionIfNeeded(
switch AVAudioSession.sharedInstance().recordPermission { presentingViewController: UIViewController? = nil,
case .granted: break onNotGranted: (() -> Void)? = nil
case .denied: ) {
onNotGranted?() switch AVAudioSession.sharedInstance().recordPermission {
let modal = PermissionMissingModal(permission: "microphone") { case .granted: break
case .denied:
guard let presentingViewController: UIViewController = (presentingViewController ?? CurrentAppContext().frontmostViewController()) else { return }
onNotGranted?() onNotGranted?()
}
modal.modalPresentationStyle = .overFullScreen let confirmationModal: ConfirmationModal = ConfirmationModal(
modal.modalTransitionStyle = .crossDissolve info: ConfirmationModal.Info(
guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } title: "Session",
presentingVC.present(modal, animated: true, completion: nil) explanation: String(
format: "modal_permission_explanation".localized(),
case .undetermined: "modal_permission_microphone".localized()
onNotGranted?() ),
AVAudioSession.sharedInstance().requestRecordPermission { _ in } confirmTitle: "modal_permission_settings_title".localized(),
dismissOnConfirm: false,
default: break onConfirm: { [weak presentingViewController] _ in
presentingViewController?.dismiss(animated: true, completion: {
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
})
},
afterClosed: { onNotGranted?() }
)
)
presentingViewController.present(confirmationModal, animated: true, completion: nil)
case .undetermined:
onNotGranted?()
AVAudioSession.sharedInstance().requestRecordPermission { _ in }
default: break
}
} }
}
public func requestLibraryPermissionIfNeeded(onAuthorized: @escaping () -> Void) { public static func requestLibraryPermissionIfNeeded(
let authorizationStatus: PHAuthorizationStatus presentingViewController: UIViewController? = nil,
if #available(iOS 14, *) { onAuthorized: @escaping () -> Void
authorizationStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite) ) {
if authorizationStatus == .notDetermined { let authorizationStatus: PHAuthorizationStatus
// When the user chooses to select photos (which is the .limit status), if #available(iOS 14, *) {
// the PHPhotoUI will present the picker view on the top of the front view. authorizationStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
// Since we have the ScreenLockUI showing when we request premissions, if authorizationStatus == .notDetermined {
// the picker view will be presented on the top of the ScreenLockUI. // When the user chooses to select photos (which is the .limit status),
// However, the ScreenLockUI will dismiss with the permission request alert view, so // the PHPhotoUI will present the picker view on the top of the front view.
// the picker view then will dismiss, too. The selection process cannot be finished // Since we have the ScreenLockUI showing when we request premissions,
// this way. So we add a flag (isRequestingPermission) to prevent the ScreenLockUI // the picker view will be presented on the top of the ScreenLockUI.
// from showing when we request the photo library permission. // However, the ScreenLockUI will dismiss with the permission request alert view, so
Environment.shared?.isRequestingPermission = true // the picker view then will dismiss, too. The selection process cannot be finished
let appMode = AppModeManager.shared.currentAppMode // this way. So we add a flag (isRequestingPermission) to prevent the ScreenLockUI
// FIXME: Rather than setting the app mode to light and then to dark again once we're done, // from showing when we request the photo library permission.
// it'd be better to just customize the appearance of the image picker. There doesn't currently Environment.shared?.isRequestingPermission = true
// appear to be a good way to do so though...
AppModeManager.shared.setCurrentAppMode(to: .light) PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in Environment.shared?.isRequestingPermission = false
DispatchQueue.main.async { if [ PHAuthorizationStatus.authorized, PHAuthorizationStatus.limited ].contains(status) {
AppModeManager.shared.setCurrentAppMode(to: appMode) onAuthorized()
} }
Environment.shared?.isRequestingPermission = false
if [ PHAuthorizationStatus.authorized, PHAuthorizationStatus.limited ].contains(status) {
onAuthorized()
} }
} }
} }
} else {
else { authorizationStatus = PHPhotoLibrary.authorizationStatus()
authorizationStatus = PHPhotoLibrary.authorizationStatus() if authorizationStatus == .notDetermined {
if authorizationStatus == .notDetermined { PHPhotoLibrary.requestAuthorization { status in
PHPhotoLibrary.requestAuthorization { status in if status == .authorized {
if status == .authorized { onAuthorized()
onAuthorized() }
} }
} }
} }
}
switch authorizationStatus {
switch authorizationStatus { case .authorized, .limited: onAuthorized()
case .authorized, .limited: onAuthorized() case .denied, .restricted:
case .denied, .restricted: guard let presentingViewController: UIViewController = (presentingViewController ?? CurrentAppContext().frontmostViewController()) else { return }
let modal = PermissionMissingModal(permission: "library") { }
modal.modalPresentationStyle = .overFullScreen let confirmationModal: ConfirmationModal = ConfirmationModal(
modal.modalTransitionStyle = .crossDissolve info: ConfirmationModal.Info(
guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } title: "Session",
presentingVC.present(modal, animated: true, completion: nil) explanation: String(
format: "modal_permission_explanation".localized(),
default: return "modal_permission_library".localized()
),
confirmTitle: "modal_permission_settings_title".localized(),
dismissOnConfirm: false
) { [weak presentingViewController] _ in
presentingViewController?.dismiss(animated: true, completion: {
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
})
}
)
presentingViewController.present(confirmationModal, animated: true, completion: nil)
default: return
}
} }
} }

View File

@ -3,10 +3,27 @@
import UIKit import UIKit
import SessionUIKit import SessionUIKit
public extension Notification.Name {
static let windowSubviewsChanged = Notification.Name("windowSubviewsChanged")
}
public class TraitObservingWindow: UIWindow { public class TraitObservingWindow: UIWindow {
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection) super.traitCollectionDidChange(previousTraitCollection)
ThemeManager.traitCollectionDidChange(previousTraitCollection) ThemeManager.traitCollectionDidChange(previousTraitCollection)
} }
public override func didAddSubview(_ subview: UIView) {
super.didAddSubview(subview)
NotificationCenter.default.post(name: .windowSubviewsChanged, object: nil)
}
public override func willRemoveSubview(_ subview: UIView) {
super.willRemoveSubview(subview)
NotificationCenter.default.post(name: .windowSubviewsChanged, object: nil)
}
} }

View File

@ -1,5 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
extension UIView { extension UIView {
func makeViewDraggable() { func makeViewDraggable() {
@ -8,29 +11,36 @@ extension UIView {
} }
@objc private func handlePanForDragging(_ gesture: UIPanGestureRecognizer) { @objc private func handlePanForDragging(_ gesture: UIPanGestureRecognizer) {
let location = gesture.location(in: self.superview!) guard let superview: UIView = self.superview else { return }
let location = gesture.location(in: superview)
if let draggedView = gesture.view { if let draggedView = gesture.view {
draggedView.center = location draggedView.center = location
if gesture.state == .ended { if gesture.state == .ended {
if draggedView.frame.midX >= self.superview!.layer.frame.width / 2 { if draggedView.frame.midX >= (superview.layer.frame.width / 2) {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: { UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: {
draggedView.center.x = self.superview!.layer.frame.width - draggedView.width() / 2 draggedView.center.x = (superview.layer.frame.width - (draggedView.width() / 2) - Values.smallSpacing)
}, completion: nil)
}else{
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: {
draggedView.center.x = draggedView.width() / 2
}, completion: nil) }, completion: nil)
} }
let topMargin = UIApplication.shared.keyWindow!.safeAreaInsets.top + Values.veryLargeSpacing else
{
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: {
draggedView.center.x = ((draggedView.width() / 2) + Values.smallSpacing)
}, completion: nil)
}
let topMargin = ((UIApplication.shared.keyWindow?.safeAreaInsets.top ?? 0) + Values.veryLargeSpacing)
if draggedView.frame.minY <= topMargin { if draggedView.frame.minY <= topMargin {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: { UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: {
draggedView.center.y = topMargin + draggedView.height() / 2 draggedView.center.y = (topMargin + (draggedView.height() / 2))
}, completion: nil) }, completion: nil)
} }
let bottomMargin = UIApplication.shared.keyWindow!.safeAreaInsets.bottom
if draggedView.frame.maxY >= self.superview!.layer.frame.height { let bottomMargin = (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0)
if draggedView.frame.maxY >= superview.layer.frame.height {
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: { UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: {
draggedView.center.y = self.superview!.layer.frame.height - draggedView.height() / 2 - bottomMargin draggedView.center.y = (superview.layer.frame.height - (draggedView.height() / 2) - bottomMargin)
}, completion: nil) }, completion: nil)
} }
} }

View File

@ -88,6 +88,10 @@ internal enum Theme_ClassicDark: ThemeColors {
.conversationButton_unreadBubbleText: .classicDark6, .conversationButton_unreadBubbleText: .classicDark6,
.conversationButton_swipeDestructive: .dangerDark, .conversationButton_swipeDestructive: .dangerDark,
.conversationButton_swipeSecondary: .classicDark2, .conversationButton_swipeSecondary: .classicDark2,
.conversationButton_swipeTertiary: Theme.PrimaryColor.orange.color .conversationButton_swipeTertiary: Theme.PrimaryColor.orange.color,
// Call
.callAccept_background: Theme.PrimaryColor.green.color,
.callDecline_background: .dangerDark
] ]
} }

View File

@ -88,6 +88,10 @@ internal enum Theme_ClassicLight: ThemeColors {
.conversationButton_unreadBubbleText: .classicLight0, .conversationButton_unreadBubbleText: .classicLight0,
.conversationButton_swipeDestructive: .dangerLight, .conversationButton_swipeDestructive: .dangerLight,
.conversationButton_swipeSecondary: .classicLight1, .conversationButton_swipeSecondary: .classicLight1,
.conversationButton_swipeTertiary: Theme.PrimaryColor.orange.color .conversationButton_swipeTertiary: Theme.PrimaryColor.orange.color,
// Call
.callAccept_background: Theme.PrimaryColor.green.color,
.callDecline_background: .dangerLight
] ]
} }

View File

@ -88,6 +88,10 @@ internal enum Theme_OceanDark: ThemeColors {
.conversationButton_unreadBubbleText: .oceanDark0, .conversationButton_unreadBubbleText: .oceanDark0,
.conversationButton_swipeDestructive: .dangerDark, .conversationButton_swipeDestructive: .dangerDark,
.conversationButton_swipeSecondary: .oceanDark2, .conversationButton_swipeSecondary: .oceanDark2,
.conversationButton_swipeTertiary: Theme.PrimaryColor.orange.color .conversationButton_swipeTertiary: Theme.PrimaryColor.orange.color,
// Call
.callAccept_background: Theme.PrimaryColor.green.color,
.callDecline_background: .dangerDark
] ]
} }

View File

@ -88,6 +88,10 @@ internal enum Theme_OceanLight: ThemeColors {
.conversationButton_unreadBubbleText: .oceanLight0, .conversationButton_unreadBubbleText: .oceanLight0,
.conversationButton_swipeDestructive: .dangerLight, .conversationButton_swipeDestructive: .dangerLight,
.conversationButton_swipeSecondary: .oceanLight1, .conversationButton_swipeSecondary: .oceanLight1,
.conversationButton_swipeTertiary: Theme.PrimaryColor.orange.color .conversationButton_swipeTertiary: Theme.PrimaryColor.orange.color,
// Call
.callAccept_background: Theme.PrimaryColor.green.color,
.callDecline_background: .dangerLight
] ]
} }

View File

@ -140,4 +140,8 @@ public enum ThemeValue {
case conversationButton_swipeDestructive case conversationButton_swipeDestructive
case conversationButton_swipeSecondary case conversationButton_swipeSecondary
case conversationButton_swipeTertiary case conversationButton_swipeTertiary
// Call
case callAccept_background
case callDecline_background
} }

View File

@ -23,6 +23,13 @@ public extension NSAttributedString {
func appending(string: String, attributes: [Key: Any]? = nil) -> NSAttributedString { func appending(string: String, attributes: [Key: Any]? = nil) -> NSAttributedString {
return appending(NSAttributedString(string: string, attributes: attributes)) return appending(NSAttributedString(string: string, attributes: attributes))
} }
func adding(attributes: [Key: Any], range: NSRange) -> NSAttributedString {
let mutableString: NSMutableAttributedString = NSMutableAttributedString(attributedString: self)
mutableString.addAttributes(attributes, range: range)
return mutableString
}
// The actual Swift implementation of 'uppercased' is pretty nuts (see // The actual Swift implementation of 'uppercased' is pretty nuts (see
// https://github.com/apple/swift/blob/main/stdlib/public/core/String.swift#L901) // https://github.com/apple/swift/blob/main/stdlib/public/core/String.swift#L901)