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:
parent
b47e5accd6
commit
b029728b6c
|
@ -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 */,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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() }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue