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 */; };
|
||||
7B0EFDF62755CC5400FFAAE7 /* CallMissedTipsModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0EFDF52755CC5400FFAAE7 /* CallMissedTipsModal.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 */; };
|
||||
7B1581E827210ECC00848B49 /* RenderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1581E727210ECC00848B49 /* RenderView.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 */; };
|
||||
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 */; };
|
||||
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 */; };
|
||||
B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8269D2825C7A4B400488AB4 /* InputView.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 */; };
|
||||
B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8F5F60225EDE16F003BF8D4 /* DataExtractionNotification.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 */; };
|
||||
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 */; };
|
||||
|
@ -482,7 +478,6 @@
|
|||
C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D662558A0170043A11F /* DiffieHellman.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 */; };
|
||||
C3A76A8D25DB83F90074CB90 /* PermissionMissingModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */; };
|
||||
C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFF125AE99710089E6DD /* AppDelegate.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 */; };
|
||||
|
@ -1173,7 +1168,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1263,8 +1257,6 @@
|
|||
B81D25B726157F20004D1FE1 /* storage-seed-1.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "storage-seed-1.crt"; sourceTree = "<group>"; };
|
||||
B81D25B826157F20004D1FE1 /* public-loki-foundation.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "public-loki-foundation.crt"; sourceTree = "<group>"; };
|
||||
B81D25B926157F20004D1FE1 /* storage-seed-3.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "storage-seed-3.crt"; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1352,7 +1344,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -1585,7 +1576,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -2359,17 +2349,12 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */,
|
||||
B8F5F72225F1B4CA003BF8D4 /* DownloadAttachmentModal.swift */,
|
||||
C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */,
|
||||
B821494525D4D6FF009C0F2A /* URLModal.swift */,
|
||||
B8AF4BB326A5204600583500 /* SendSeedModal.swift */,
|
||||
B8758859264503A6000E60D0 /* JoinOpenGroupModal.swift */,
|
||||
B82149B725D60393009C0F2A /* BlockedModal.swift */,
|
||||
B82149C025D605C6009C0F2A /* InfoBanner.swift */,
|
||||
C374EEEA25DA3CA70073A857 /* ConversationTitleView.swift */,
|
||||
B848A4C4269EAAA200617031 /* UserDetailsSheet.swift */,
|
||||
FD4B200D283492210034334B /* InsetLockableTableView.swift */,
|
||||
7B1581E3271FC59C00848B49 /* CallModal.swift */,
|
||||
7B9F71C828470667006DFE7B /* ReactionListSheet.swift */,
|
||||
);
|
||||
path = "Views & Modals";
|
||||
|
@ -5503,7 +5488,6 @@
|
|||
FD09C5E828264937000CE219 /* MediaDetailViewController.swift in Sources */,
|
||||
4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */,
|
||||
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */,
|
||||
C3A76A8D25DB83F90074CB90 /* PermissionMissingModal.swift in Sources */,
|
||||
7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */,
|
||||
B849789625D4A2F500D0D0B3 /* LinkPreviewView.swift in Sources */,
|
||||
7B1B52DF28580D51006069F2 /* EmojiPickerCollectionView.swift in Sources */,
|
||||
|
@ -5519,7 +5503,6 @@
|
|||
4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */,
|
||||
FD87DCFE28B7582C00AF0F98 /* BlockedContactsViewModel.swift in Sources */,
|
||||
FD37E9DD28A384EB003AE748 /* PrimaryColorSelectionView.swift in Sources */,
|
||||
B82149B825D60393009C0F2A /* BlockedModal.swift in Sources */,
|
||||
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */,
|
||||
FDFDE126282D05380098B17F /* MediaInteractiveDismiss.swift in Sources */,
|
||||
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
|
||||
|
@ -5531,7 +5514,6 @@
|
|||
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
|
||||
C331FFFE2558FF3B00070591 /* FullConversationCell.swift in Sources */,
|
||||
FD52090728B49738006098F6 /* ConfirmationModal.swift in Sources */,
|
||||
B8F5F72325F1B4CA003BF8D4 /* DownloadAttachmentModal.swift in Sources */,
|
||||
C3DFFAC623E96F0D0058DAF8 /* Sheet.swift in Sources */,
|
||||
B8D84EA325DF745A005A043E /* LinkPreviewState.swift in Sources */,
|
||||
45C0DC1E1E69011F00E04C47 /* UIStoryboard+OWS.swift in Sources */,
|
||||
|
@ -5540,7 +5522,6 @@
|
|||
B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */,
|
||||
7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */,
|
||||
B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */,
|
||||
B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */,
|
||||
B877E24226CA12910007970A /* CallVC.swift in Sources */,
|
||||
7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */,
|
||||
C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */,
|
||||
|
@ -5597,7 +5578,6 @@
|
|||
4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */,
|
||||
C328253025CA55370062D0A7 /* ContextMenuWindow.swift in Sources */,
|
||||
340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */,
|
||||
7B1581E4271FC59D00848B49 /* CallModal.swift in Sources */,
|
||||
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
|
||||
B84664F5235022F30083A1CD /* MentionUtilities.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 SessionUIKit
|
||||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
import UIKit
|
||||
import MediaPlayer
|
||||
|
||||
final class CallVC: UIViewController, VideoPreviewDelegate {
|
||||
let call: SessionCall
|
||||
|
@ -19,36 +21,51 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
return result
|
||||
}()
|
||||
|
||||
// MARK: UI Components
|
||||
// MARK: - UI Components
|
||||
|
||||
private lazy var localVideoView: LocalVideoView = {
|
||||
let result = LocalVideoView()
|
||||
result.clipsToBounds = true
|
||||
result.themeBackgroundColor = .backgroundSecondary
|
||||
result.isHidden = !call.isVideoEnabled
|
||||
result.layer.cornerRadius = 10
|
||||
result.layer.masksToBounds = true
|
||||
result.set(.width, to: LocalVideoView.width)
|
||||
result.set(.height, to: LocalVideoView.height)
|
||||
result.makeViewDraggable()
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var remoteVideoView: RemoteVideoView = {
|
||||
let result = RemoteVideoView()
|
||||
result.alpha = 0
|
||||
result.backgroundColor = .black
|
||||
result.themeBackgroundColor = .backgroundPrimary
|
||||
result.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleRemoteVieioViewTapped)))
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var fadeView: UIView = {
|
||||
let height: CGFloat = ((UIApplication.shared.keyWindow?.safeAreaInsets.top).map { $0 + Values.veryLargeSpacing } ?? 64)
|
||||
|
||||
let result = UIView()
|
||||
let height: CGFloat = 64
|
||||
var frame = UIScreen.main.bounds
|
||||
frame.size.height = height
|
||||
|
||||
let layer = CAGradientLayer()
|
||||
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.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
|
||||
}()
|
||||
|
||||
|
@ -61,42 +78,60 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
result.layer.cornerRadius = radius
|
||||
result.layer.masksToBounds = true
|
||||
result.contentMode = .scaleAspectFill
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var minimizeButton: UIButton = {
|
||||
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
|
||||
let image = UIImage(named: "Minimize")!.withTint(.white)
|
||||
result.setImage(image, for: UIControl.State.normal)
|
||||
result.set(.width, to: 60)
|
||||
result.set(.height, to: 60)
|
||||
result.addTarget(self, action: #selector(minimize), for: UIControl.Event.touchUpInside)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var answerButton: UIButton = {
|
||||
let result = UIButton(type: .custom)
|
||||
result.isHidden = call.hasStartedConnecting
|
||||
let image = UIImage(named: "AnswerCall")!.withTint(.white)
|
||||
result.setImage(image, for: UIControl.State.normal)
|
||||
result.set(.width, to: 60)
|
||||
result.set(.height, to: 60)
|
||||
result.backgroundColor = Colors.accent
|
||||
result.setImage(
|
||||
UIImage(named: "AnswerCall")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
for: .normal
|
||||
)
|
||||
result.themeTintColor = .white
|
||||
result.themeBackgroundColor = .callAccept_background
|
||||
result.layer.cornerRadius = 30
|
||||
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
|
||||
}()
|
||||
|
||||
private lazy var hangUpButton: UIButton = {
|
||||
let result = UIButton(type: .custom)
|
||||
let image = UIImage(named: "EndCall")!.withTint(.white)
|
||||
result.setImage(image, for: UIControl.State.normal)
|
||||
result.set(.width, to: 60)
|
||||
result.set(.height, to: 60)
|
||||
result.backgroundColor = Colors.destructive
|
||||
result.setImage(
|
||||
UIImage(named: "EndCall")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
for: .normal
|
||||
)
|
||||
result.themeTintColor = .white
|
||||
result.themeBackgroundColor = .callDecline_background
|
||||
result.layer.cornerRadius = 30
|
||||
result.addTarget(self, action: #selector(endCall), for: UIControl.Event.touchUpInside)
|
||||
result.set(.width, to: 60)
|
||||
result.set(.height, to: 60)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
|
@ -104,58 +139,83 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
let result = UIStackView(arrangedSubviews: [hangUpButton, answerButton])
|
||||
result.axis = .horizontal
|
||||
result.spacing = Values.veryLargeSpacing * 2 + 40
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var switchCameraButton: UIButton = {
|
||||
let result = UIButton(type: .custom)
|
||||
result.isEnabled = call.isVideoEnabled
|
||||
let image = UIImage(named: "SwitchCamera")!.withTint(.white)
|
||||
result.setImage(image, for: UIControl.State.normal)
|
||||
result.set(.width, to: 60)
|
||||
result.set(.height, to: 60)
|
||||
result.backgroundColor = UIColor(hex: 0x1F1F1F)
|
||||
result.setImage(
|
||||
UIImage(named: "SwitchCamera")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
for: .normal
|
||||
)
|
||||
result.themeTintColor = .textPrimary
|
||||
result.themeBackgroundColor = .backgroundSecondary
|
||||
result.layer.cornerRadius = 30
|
||||
result.addTarget(self, action: #selector(switchCamera), for: UIControl.Event.touchUpInside)
|
||||
result.set(.width, to: 60)
|
||||
result.set(.height, to: 60)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var switchAudioButton: UIButton = {
|
||||
let result = UIButton(type: .custom)
|
||||
let image = UIImage(named: "AudioOff")!.withTint(.white)
|
||||
result.setImage(image, for: UIControl.State.normal)
|
||||
result.set(.width, to: 60)
|
||||
result.set(.height, to: 60)
|
||||
result.backgroundColor = call.isMuted ? Colors.destructive : UIColor(hex: 0x1F1F1F)
|
||||
result.setImage(
|
||||
UIImage(named: "AudioOff")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
for: .normal
|
||||
)
|
||||
result.themeTintColor = (call.isMuted ?
|
||||
.white :
|
||||
.textPrimary
|
||||
)
|
||||
result.themeBackgroundColor = (call.isMuted ?
|
||||
.danger :
|
||||
.backgroundSecondary
|
||||
)
|
||||
result.layer.cornerRadius = 30
|
||||
result.addTarget(self, action: #selector(switchAudio), for: UIControl.Event.touchUpInside)
|
||||
result.set(.width, to: 60)
|
||||
result.set(.height, to: 60)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var videoButton: UIButton = {
|
||||
let result = UIButton(type: .custom)
|
||||
let image = UIImage(named: "VideoCall")?.withRenderingMode(.alwaysTemplate)
|
||||
result.setImage(image, for: UIControl.State.normal)
|
||||
result.set(.width, to: 60)
|
||||
result.set(.height, to: 60)
|
||||
result.tintColor = .white
|
||||
result.backgroundColor = UIColor(hex: 0x1F1F1F)
|
||||
result.setImage(
|
||||
UIImage(named: "VideoCall")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
for: .normal
|
||||
)
|
||||
result.themeTintColor = .textPrimary
|
||||
result.themeBackgroundColor = .backgroundSecondary
|
||||
result.layer.cornerRadius = 30
|
||||
result.addTarget(self, action: #selector(operateCamera), for: UIControl.Event.touchUpInside)
|
||||
result.set(.width, to: 60)
|
||||
result.set(.height, to: 60)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var volumeView: MPVolumeView = {
|
||||
let result = MPVolumeView()
|
||||
let image = UIImage(named: "Speaker")?.withRenderingMode(.alwaysTemplate)
|
||||
result.showsVolumeSlider = false
|
||||
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(.height, to: 60)
|
||||
result.tintColor = .white
|
||||
result.backgroundColor = UIColor(hex: 0x1F1F1F)
|
||||
result.layer.cornerRadius = 30
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
|
@ -163,37 +223,43 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
let result = UIStackView(arrangedSubviews: [switchCameraButton, videoButton, switchAudioButton, volumeView])
|
||||
result.axis = .horizontal
|
||||
result.spacing = Values.veryLargeSpacing
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = .white
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
result.themeTextColor = .textPrimary
|
||||
result.textAlignment = .center
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var callInfoLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.isHidden = call.hasConnected
|
||||
result.textColor = .white
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
result.themeTextColor = .textPrimary
|
||||
result.textAlignment = .center
|
||||
result.isHidden = call.hasConnected
|
||||
|
||||
if call.hasStartedConnecting { result.text = "Connecting..." }
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var callDurationLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.isHidden = true
|
||||
result.textColor = .white
|
||||
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
result.themeTextColor = .textPrimary
|
||||
result.textAlignment = .center
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
// MARK: - Lifecycle
|
||||
|
||||
init(for call: SessionCall) {
|
||||
self.call = call
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
@ -208,6 +274,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
UIView.animate(withDuration: 0.25) {
|
||||
self.remoteVideoView.alpha = isEnabled ? 1 : 0
|
||||
}
|
||||
|
||||
if self.callInfoLabel.alpha < 0.5 {
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.operationPanel.alpha = 1
|
||||
|
@ -217,45 +284,60 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.call.hasStartedConnectingDidChange = {
|
||||
DispatchQueue.main.async {
|
||||
self.callInfoLabel.text = "Connecting..."
|
||||
self.answerButton.alpha = 0
|
||||
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: {
|
||||
self.answerButton.isHidden = true
|
||||
}, completion: nil)
|
||||
|
||||
UIView.animate(
|
||||
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 {
|
||||
CallRingTonePlayer.shared.stopPlayingRingTone()
|
||||
self.callInfoLabel.text = "Connected"
|
||||
self.minimizeButton.isHidden = false
|
||||
self.durationTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
||||
self.updateDuration()
|
||||
|
||||
self?.callInfoLabel.text = "Connected"
|
||||
self?.minimizeButton.isHidden = false
|
||||
self?.durationTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
|
||||
self?.updateDuration()
|
||||
}
|
||||
self.callInfoLabel.isHidden = true
|
||||
self.callDurationLabel.isHidden = false
|
||||
self?.callInfoLabel.isHidden = true
|
||||
self?.callDurationLabel.isHidden = false
|
||||
}
|
||||
}
|
||||
self.call.hasEndedDidChange = {
|
||||
|
||||
self.call.hasEndedDidChange = { [weak self] in
|
||||
DispatchQueue.main.async {
|
||||
self.durationTimer?.invalidate()
|
||||
self.durationTimer = nil
|
||||
self.handleEndCallMessage()
|
||||
self?.durationTimer?.invalidate()
|
||||
self?.durationTimer = nil
|
||||
self?.handleEndCallMessage()
|
||||
}
|
||||
}
|
||||
self.call.hasStartedReconnecting = {
|
||||
|
||||
self.call.hasStartedReconnecting = { [weak self] in
|
||||
DispatchQueue.main.async {
|
||||
self.callInfoLabel.isHidden = false
|
||||
self.callDurationLabel.isHidden = true
|
||||
self.callInfoLabel.text = "Reconnecting..."
|
||||
self?.callInfoLabel.isHidden = false
|
||||
self?.callDurationLabel.isHidden = true
|
||||
self?.callInfoLabel.text = "Reconnecting..."
|
||||
}
|
||||
}
|
||||
self.call.hasReconnected = {
|
||||
|
||||
self.call.hasReconnected = { [weak self] in
|
||||
DispatchQueue.main.async {
|
||||
self.callInfoLabel.isHidden = true
|
||||
self.callDurationLabel.isHidden = false
|
||||
self?.callInfoLabel.isHidden = true
|
||||
self?.callDurationLabel.isHidden = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -264,19 +346,24 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .black
|
||||
|
||||
view.themeBackgroundColor = .backgroundPrimary
|
||||
|
||||
setUpViewHierarchy()
|
||||
|
||||
if shouldRestartCamera { cameraManager.prepare() }
|
||||
|
||||
touch(call.videoCapturer)
|
||||
titleLabel.text = self.call.contactName
|
||||
AppEnvironment.shared.callManager.startCall(call) { error in
|
||||
AppEnvironment.shared.callManager.startCall(call) { [weak self] error in
|
||||
DispatchQueue.main.async {
|
||||
if let _ = error {
|
||||
self.callInfoLabel.text = "Can't start a call."
|
||||
self.endCall()
|
||||
} else {
|
||||
self.callInfoLabel.text = "Ringing..."
|
||||
self.answerButton.isHidden = true
|
||||
self?.callInfoLabel.text = "Can't start a call."
|
||||
self?.endCall()
|
||||
}
|
||||
else {
|
||||
self?.callInfoLabel.text = "Ringing..."
|
||||
self?.answerButton.isHidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -293,41 +380,50 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
// Profile picture container
|
||||
let profilePictureContainer = UIView()
|
||||
view.addSubview(profilePictureContainer)
|
||||
|
||||
// Remote video view
|
||||
call.attachRemoteVideoRenderer(remoteVideoView)
|
||||
view.addSubview(remoteVideoView)
|
||||
remoteVideoView.translatesAutoresizingMaskIntoConstraints = false
|
||||
remoteVideoView.pin(to: view)
|
||||
|
||||
// Local video view
|
||||
call.attachLocalVideoRenderer(localVideoView)
|
||||
|
||||
// Fade view
|
||||
view.addSubview(fadeView)
|
||||
fadeView.translatesAutoresizingMaskIntoConstraints = false
|
||||
fadeView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: view)
|
||||
|
||||
// Minimize button
|
||||
view.addSubview(minimizeButton)
|
||||
minimizeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
minimizeButton.pin(.left, to: .left, of: view)
|
||||
minimizeButton.pin(.top, to: .top, of: view, withInset: 32)
|
||||
|
||||
// Title label
|
||||
view.addSubview(titleLabel)
|
||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
titleLabel.center(.vertical, in: minimizeButton)
|
||||
titleLabel.center(.horizontal, in: view)
|
||||
|
||||
// Response Panel
|
||||
view.addSubview(responsePanel)
|
||||
responsePanel.center(.horizontal, in: view)
|
||||
responsePanel.pin(.bottom, to: .bottom, of: view, withInset: -Values.newConversationButtonBottomOffset)
|
||||
|
||||
// Operation Panel
|
||||
view.addSubview(operationPanel)
|
||||
operationPanel.center(.horizontal, in: view)
|
||||
operationPanel.pin(.bottom, to: .top, of: responsePanel, withInset: -Values.veryLargeSpacing)
|
||||
|
||||
// Profile picture view
|
||||
profilePictureContainer.pin(.top, to: .bottom, of: fadeView)
|
||||
profilePictureContainer.pin(.bottom, to: .top, of: operationPanel)
|
||||
profilePictureContainer.pin([ UIView.HorizontalEdge.left, UIView.HorizontalEdge.right ], to: view)
|
||||
profilePictureContainer.addSubview(profilePictureView)
|
||||
profilePictureView.center(in: profilePictureContainer)
|
||||
|
||||
// Call info label
|
||||
let callInfoLabelContainer = UIView()
|
||||
view.addSubview(callInfoLabelContainer)
|
||||
|
@ -343,25 +439,28 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
}
|
||||
|
||||
private func addLocalVideoView() {
|
||||
let safeAreaInsets = UIApplication.shared.keyWindow!.safeAreaInsets
|
||||
let window = CurrentAppContext().mainWindow!
|
||||
window.addSubview(localVideoView)
|
||||
let safeAreaInsets = UIApplication.shared.keyWindow?.safeAreaInsets
|
||||
CurrentAppContext().mainWindow?.addSubview(localVideoView)
|
||||
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)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.start() }
|
||||
|
||||
shouldRestartCamera = true
|
||||
addLocalVideoView()
|
||||
remoteVideoView.alpha = call.isRemoteVideoEnabled ? 1 : 0
|
||||
remoteVideoView.alpha = (call.isRemoteVideoEnabled ? 1 : 0)
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
if (call.isVideoEnabled && shouldRestartCamera) { cameraManager.stop() }
|
||||
|
||||
localVideoView.removeFromSuperview()
|
||||
}
|
||||
|
||||
|
@ -373,9 +472,9 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
}
|
||||
|
||||
@objc func didChangeDeviceOrientation(notification: Notification) {
|
||||
|
||||
func rotateAllButtons(rotationAngle: CGFloat) {
|
||||
let transform = CGAffineTransform(rotationAngle: rotationAngle)
|
||||
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
self.answerButton.transform = transform
|
||||
self.hangUpButton.transform = transform
|
||||
|
@ -387,16 +486,11 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
}
|
||||
|
||||
switch UIDevice.current.orientation {
|
||||
case .portrait:
|
||||
rotateAllButtons(rotationAngle: 0)
|
||||
case .portraitUpsideDown:
|
||||
rotateAllButtons(rotationAngle: .pi)
|
||||
case .landscapeLeft:
|
||||
rotateAllButtons(rotationAngle: .halfPi)
|
||||
case .landscapeRight:
|
||||
rotateAllButtons(rotationAngle: .pi + .halfPi)
|
||||
default:
|
||||
break
|
||||
case .portrait: rotateAllButtons(rotationAngle: 0)
|
||||
case .portraitUpsideDown: rotateAllButtons(rotationAngle: .pi)
|
||||
case .landscapeLeft: rotateAllButtons(rotationAngle: .halfPi)
|
||||
case .landscapeRight: rotateAllButtons(rotationAngle: .pi + .halfPi)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -409,39 +503,42 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
SNLog("[Calls] Ending call.")
|
||||
self.callInfoLabel.isHidden = false
|
||||
self.callDurationLabel.isHidden = true
|
||||
callInfoLabel.text = "Call Ended"
|
||||
self.callInfoLabel.text = "Call Ended"
|
||||
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.remoteVideoView.alpha = 0
|
||||
self.operationPanel.alpha = 1
|
||||
self.responsePanel.alpha = 1
|
||||
self.callInfoLabel.alpha = 1
|
||||
}
|
||||
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
|
||||
self.conversationVC?.showInputAccessoryView()
|
||||
self.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
|
||||
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { [weak self] _ in
|
||||
self?.conversationVC?.showInputAccessoryView()
|
||||
self?.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func answerCall() {
|
||||
AppEnvironment.shared.callManager.answerCall(call) { error in
|
||||
AppEnvironment.shared.callManager.answerCall(call) { [weak self] error in
|
||||
DispatchQueue.main.async {
|
||||
if let _ = error {
|
||||
self.callInfoLabel.text = "Can't answer the call."
|
||||
self.endCall()
|
||||
self?.callInfoLabel.text = "Can't answer the call."
|
||||
self?.endCall()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func endCall() {
|
||||
AppEnvironment.shared.callManager.endCall(call) { error in
|
||||
AppEnvironment.shared.callManager.endCall(call) { [weak self] error in
|
||||
if let _ = error {
|
||||
self.call.endSessionCall()
|
||||
self?.call.endSessionCall()
|
||||
AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: nil)
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.conversationVC?.showInputAccessoryView()
|
||||
self.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
self?.conversationVC?.showInputAccessoryView()
|
||||
self?.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -451,7 +548,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
duration += 1
|
||||
}
|
||||
|
||||
// MARK: Minimize to a floating view
|
||||
// MARK: - Minimize to a floating view
|
||||
|
||||
@objc private func minimize() {
|
||||
self.shouldRestartCamera = false
|
||||
let miniCallView = MiniCallView(from: self)
|
||||
|
@ -460,17 +558,19 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: Video and Audio
|
||||
// MARK: - Video and Audio
|
||||
|
||||
@objc private func operateCamera() {
|
||||
if (call.isVideoEnabled) {
|
||||
localVideoView.isHidden = true
|
||||
cameraManager.stop()
|
||||
videoButton.tintColor = .white
|
||||
videoButton.backgroundColor = UIColor(hex: 0x1F1F1F)
|
||||
videoButton.themeTintColor = .textPrimary
|
||||
videoButton.themeBackgroundColor = .backgroundSecondary
|
||||
switchCameraButton.isEnabled = false
|
||||
call.isVideoEnabled = false
|
||||
} else {
|
||||
guard requestCameraPermissionIfNeeded() else { return }
|
||||
}
|
||||
else {
|
||||
guard Permissions.requestCameraPermissionIfNeeded() else { return }
|
||||
let previewVC = VideoPreviewVC()
|
||||
previewVC.delegate = self
|
||||
present(previewVC, animated: true, completion: nil)
|
||||
|
@ -481,8 +581,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
localVideoView.isHidden = false
|
||||
cameraManager.prepare()
|
||||
cameraManager.start()
|
||||
videoButton.tintColor = UIColor(hex: 0x1F1F1F)
|
||||
videoButton.backgroundColor = .white
|
||||
videoButton.themeTintColor = .backgroundSecondary
|
||||
videoButton.themeBackgroundColor = .textPrimary
|
||||
switchCameraButton.isEnabled = true
|
||||
call.isVideoEnabled = true
|
||||
}
|
||||
|
@ -493,10 +593,13 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
|
||||
@objc private func switchAudio() {
|
||||
if call.isMuted {
|
||||
switchAudioButton.backgroundColor = UIColor(hex: 0x1F1F1F)
|
||||
switchAudioButton.themeTintColor = .textPrimary
|
||||
switchAudioButton.themeBackgroundColor = .backgroundSecondary
|
||||
call.isMuted = false
|
||||
} else {
|
||||
switchAudioButton.backgroundColor = Colors.destructive
|
||||
}
|
||||
else {
|
||||
switchAudioButton.themeTintColor = .white
|
||||
switchAudioButton.themeBackgroundColor = .danger
|
||||
call.isMuted = true
|
||||
}
|
||||
}
|
||||
|
@ -506,41 +609,48 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
let currentRoute = currentSession.currentRoute
|
||||
if let currentOutput = currentRoute.outputs.first {
|
||||
if let latestKnownAudioOutputDeviceName = latestKnownAudioOutputDeviceName, currentOutput.portName == latestKnownAudioOutputDeviceName { return }
|
||||
|
||||
latestKnownAudioOutputDeviceName = currentOutput.portName
|
||||
|
||||
switch currentOutput.portType {
|
||||
case .builtInSpeaker:
|
||||
let image = UIImage(named: "Speaker")?.withRenderingMode(.alwaysTemplate)
|
||||
volumeView.setRouteButtonImage(image, for: .normal)
|
||||
volumeView.tintColor = UIColor(hex: 0x1F1F1F)
|
||||
volumeView.backgroundColor = .white
|
||||
case .headphones:
|
||||
let image = UIImage(named: "Headsets")?.withRenderingMode(.alwaysTemplate)
|
||||
volumeView.setRouteButtonImage(image, for: .normal)
|
||||
volumeView.tintColor = UIColor(hex: 0x1F1F1F)
|
||||
volumeView.backgroundColor = .white
|
||||
case .bluetoothLE: fallthrough
|
||||
case .bluetoothA2DP:
|
||||
let image = UIImage(named: "Bluetooth")?.withRenderingMode(.alwaysTemplate)
|
||||
volumeView.setRouteButtonImage(image, for: .normal)
|
||||
volumeView.tintColor = UIColor(hex: 0x1F1F1F)
|
||||
volumeView.backgroundColor = .white
|
||||
case .bluetoothHFP:
|
||||
let image = UIImage(named: "Airpods")?.withRenderingMode(.alwaysTemplate)
|
||||
volumeView.setRouteButtonImage(image, for: .normal)
|
||||
volumeView.tintColor = UIColor(hex: 0x1F1F1F)
|
||||
volumeView.backgroundColor = .white
|
||||
case .builtInReceiver: fallthrough
|
||||
default:
|
||||
let image = UIImage(named: "Speaker")?.withRenderingMode(.alwaysTemplate)
|
||||
volumeView.setRouteButtonImage(image, for: .normal)
|
||||
volumeView.tintColor = .white
|
||||
volumeView.backgroundColor = UIColor(hex: 0x1F1F1F)
|
||||
case .builtInSpeaker:
|
||||
let image = UIImage(named: "Speaker")?.withRenderingMode(.alwaysTemplate)
|
||||
volumeView.setRouteButtonImage(image, for: .normal)
|
||||
volumeView.themeTintColor = .backgroundSecondary
|
||||
volumeView.themeBackgroundColor = .textPrimary
|
||||
|
||||
case .headphones:
|
||||
let image = UIImage(named: "Headsets")?.withRenderingMode(.alwaysTemplate)
|
||||
volumeView.setRouteButtonImage(image, for: .normal)
|
||||
volumeView.themeTintColor = .backgroundSecondary
|
||||
volumeView.themeBackgroundColor = .textPrimary
|
||||
|
||||
case .bluetoothLE: fallthrough
|
||||
case .bluetoothA2DP:
|
||||
let image = UIImage(named: "Bluetooth")?.withRenderingMode(.alwaysTemplate)
|
||||
volumeView.setRouteButtonImage(image, for: .normal)
|
||||
volumeView.themeTintColor = .backgroundSecondary
|
||||
volumeView.themeBackgroundColor = .textPrimary
|
||||
|
||||
case .bluetoothHFP:
|
||||
let image = UIImage(named: "Airpods")?.withRenderingMode(.alwaysTemplate)
|
||||
volumeView.setRouteButtonImage(image, for: .normal)
|
||||
volumeView.themeTintColor = .backgroundSecondary
|
||||
volumeView.themeBackgroundColor = .textPrimary
|
||||
|
||||
case .builtInReceiver: fallthrough
|
||||
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) {
|
||||
let isHidden = callDurationLabel.alpha < 0.5
|
||||
|
||||
UIView.animate(withDuration: 0.5) {
|
||||
self.operationPanel.alpha = isHidden ? 1 : 0
|
||||
self.responsePanel.alpha = isHidden ? 1 : 0
|
||||
|
|
|
@ -47,16 +47,24 @@ final class CameraManager : NSObject {
|
|||
|
||||
func start() {
|
||||
guard !isCapturing else { return }
|
||||
print("[Calls] Starting camera.")
|
||||
isCapturing = true
|
||||
captureSession.startRunning()
|
||||
|
||||
// Note: The 'startRunning' task is blocking so we want to do it on a non-main thread
|
||||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
||||
print("[Calls] Starting camera.")
|
||||
self?.isCapturing = true
|
||||
self?.captureSession.startRunning()
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
guard isCapturing else { return }
|
||||
print("[Calls] Stopping camera.")
|
||||
isCapturing = false
|
||||
captureSession.stopRunning()
|
||||
|
||||
// Note: The 'stopRunning' task is blocking so we want to do it on a non-main thread
|
||||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
||||
print("[Calls] Stopping camera.")
|
||||
self?.isCapturing = false
|
||||
self?.captureSession.stopRunning()
|
||||
}
|
||||
}
|
||||
|
||||
func switchCamera() {
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import WebRTC
|
||||
import SessionUIKit
|
||||
|
||||
public protocol VideoPreviewDelegate : AnyObject {
|
||||
public protocol VideoPreviewDelegate: AnyObject {
|
||||
func cameraDidConfirmTurningOn()
|
||||
}
|
||||
|
||||
|
@ -11,61 +14,89 @@ class VideoPreviewVC: UIViewController, CameraManagerDelegate {
|
|||
lazy var cameraManager: CameraManager = {
|
||||
let result = CameraManager()
|
||||
result.delegate = self
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: UI Components
|
||||
// MARK: - UI Components
|
||||
|
||||
private lazy var renderView: RenderView = {
|
||||
let result = RenderView()
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var fadeView: UIView = {
|
||||
let height: CGFloat = ((UIApplication.shared.keyWindow?.safeAreaInsets.top).map { $0 + Values.veryLargeSpacing } ?? 64)
|
||||
|
||||
let result = UIView()
|
||||
let height: CGFloat = 64
|
||||
var frame = UIScreen.main.bounds
|
||||
frame.size.height = height
|
||||
|
||||
let layer = CAGradientLayer()
|
||||
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.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
|
||||
}()
|
||||
|
||||
private lazy var closeButton: UIButton = {
|
||||
let result = UIButton(type: .custom)
|
||||
let image = UIImage(named: "X")!.withTint(.white)
|
||||
result.setImage(image, for: UIControl.State.normal)
|
||||
result.setImage(
|
||||
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(.height, to: 60)
|
||||
result.addTarget(self, action: #selector(cancel), for: UIControl.Event.touchUpInside)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var confirmButton: UIButton = {
|
||||
let result = UIButton(type: .custom)
|
||||
let image = UIImage(named: "Check")!.withTint(.white)
|
||||
result.setImage(image, for: UIControl.State.normal)
|
||||
result.setImage(
|
||||
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(.height, to: 60)
|
||||
result.addTarget(self, action: #selector(confirm), for: UIControl.Event.touchUpInside)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.text = "Preview"
|
||||
result.textColor = .white
|
||||
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
result.text = "Preview"
|
||||
result.themeTextColor = .textPrimary
|
||||
result.textAlignment = .center
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .black
|
||||
|
||||
view.themeBackgroundColor = .backgroundPrimary
|
||||
|
||||
setUpViewHierarchy()
|
||||
cameraManager.prepare()
|
||||
}
|
||||
|
@ -75,20 +106,24 @@ class VideoPreviewVC: UIViewController, CameraManagerDelegate {
|
|||
view.addSubview(renderView)
|
||||
renderView.translatesAutoresizingMaskIntoConstraints = false
|
||||
renderView.pin(to: view)
|
||||
|
||||
// Fade view
|
||||
view.addSubview(fadeView)
|
||||
fadeView.translatesAutoresizingMaskIntoConstraints = false
|
||||
fadeView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: view)
|
||||
|
||||
// Close button
|
||||
view.addSubview(closeButton)
|
||||
closeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
closeButton.pin(.left, to: .left, of: view)
|
||||
closeButton.center(.vertical, in: fadeView)
|
||||
|
||||
// Confirm button
|
||||
view.addSubview(confirmButton)
|
||||
confirmButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
confirmButton.pin(.right, to: .right, of: view)
|
||||
confirmButton.center(.vertical, in: fadeView)
|
||||
|
||||
// Title label
|
||||
view.addSubview(titleLabel)
|
||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -98,15 +133,18 @@ class VideoPreviewVC: UIViewController, CameraManagerDelegate {
|
|||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
cameraManager.start()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
cameraManager.stop()
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
// MARK: - Interaction
|
||||
|
||||
@objc func confirm() {
|
||||
delegate?.cameraDidConfirmTurningOn()
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
|
@ -116,7 +154,8 @@ class VideoPreviewVC: UIViewController, CameraManagerDelegate {
|
|||
self.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
// MARK: CameraManagerDelegate
|
||||
// MARK: - CameraManagerDelegate
|
||||
|
||||
func handleVideoOutputCaptured(sampleBuffer: CMSampleBuffer) {
|
||||
renderView.enqueue(sampleBuffer: sampleBuffer)
|
||||
}
|
||||
|
|
|
@ -71,18 +71,14 @@ final class CallMissedTipsModal: Modal {
|
|||
init(caller: String) {
|
||||
self.caller = caller
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
super.init()
|
||||
|
||||
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.")
|
||||
preconditionFailure("Use init(caller:) instead.")
|
||||
}
|
||||
|
||||
override func populateContentView() {
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import WebRTC
|
||||
import SessionUIKit
|
||||
|
||||
final class MiniCallView: UIView, RTCVideoViewDelegate {
|
||||
var callVC: CallVC
|
||||
|
||||
// MARK: UI
|
||||
private static let defaultSize: CGFloat = 100
|
||||
private let topMargin = UIApplication.shared.keyWindow!.safeAreaInsets.top + Values.veryLargeSpacing
|
||||
private let bottomMargin = UIApplication.shared.keyWindow!.safeAreaInsets.bottom
|
||||
private let topMargin = (UIApplication.shared.keyWindow?.safeAreaInsets.top ?? 0) + Values.veryLargeSpacing
|
||||
private let bottomMargin = (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0)
|
||||
|
||||
private var width: NSLayoutConstraint?
|
||||
private var height: NSLayoutConstraint?
|
||||
|
@ -16,40 +19,54 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
|
|||
private var top: 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)
|
||||
// Note: 'RTCMTLVideoView' doesn't seem to work on the simulator so use 'RTCEAGLVideoView' instead
|
||||
private lazy var remoteVideoView: RTCEAGLVideoView = {
|
||||
let result = RTCEAGLVideoView()
|
||||
result.delegate = self
|
||||
result.alpha = self.callVC.call.isRemoteVideoEnabled ? 1 : 0
|
||||
result.backgroundColor = .black
|
||||
result.themeBackgroundColor = .backgroundSecondary
|
||||
result.alpha = (self.callVC.call.isRemoteVideoEnabled ? 1 : 0)
|
||||
|
||||
return result
|
||||
}()
|
||||
#else
|
||||
private lazy var remoteVideoView: RTCMTLVideoView = {
|
||||
let result = RTCMTLVideoView()
|
||||
result.delegate = self
|
||||
result.alpha = self.callVC.call.isRemoteVideoEnabled ? 1 : 0
|
||||
result.videoContentMode = .scaleAspectFit
|
||||
result.backgroundColor = .black
|
||||
result.themeBackgroundColor = .backgroundSecondary
|
||||
result.alpha = (self.callVC.call.isRemoteVideoEnabled ? 1 : 0)
|
||||
|
||||
return result
|
||||
}()
|
||||
#endif
|
||||
|
||||
// MARK: Initialization
|
||||
// MARK: - Initialization
|
||||
|
||||
public static var current: MiniCallView?
|
||||
|
||||
init(from callVC: CallVC) {
|
||||
self.callVC = callVC
|
||||
|
||||
super.init(frame: CGRect.zero)
|
||||
self.backgroundColor = UIColor.init(white: 0, alpha: 0.8)
|
||||
|
||||
setUpViewHierarchy()
|
||||
setUpGestureRecognizers()
|
||||
MiniCallView.current = self
|
||||
|
||||
self.callVC.call.remoteVideoStateDidChange = { isEnabled in
|
||||
DispatchQueue.main.async {
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.remoteVideoView.alpha = isEnabled ? 1 : 0
|
||||
|
||||
if !isEnabled {
|
||||
self.width?.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) {
|
||||
|
@ -67,15 +91,21 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
|
|||
preconditionFailure("Use init(coder:) instead.")
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
self.clipsToBounds = true
|
||||
self.layer.cornerRadius = 10
|
||||
self.width = self.set(.width, to: MiniCallView.defaultSize)
|
||||
self.height = self.set(.height, to: MiniCallView.defaultSize)
|
||||
self.layer.cornerRadius = 10
|
||||
self.layer.masksToBounds = true
|
||||
|
||||
// Background
|
||||
let background = getBackgroudView()
|
||||
self.addSubview(background)
|
||||
background.pin(to: self)
|
||||
|
||||
// Remote video view
|
||||
callVC.call.attachRemoteVideoRenderer(remoteVideoView)
|
||||
self.addSubview(remoteVideoView)
|
||||
|
@ -84,17 +114,25 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
|
|||
}
|
||||
|
||||
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()
|
||||
imageView.layer.cornerRadius = 32
|
||||
imageView.layer.masksToBounds = true
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.image = callVC.call.profilePicture
|
||||
background.addSubview(imageView)
|
||||
result.addSubview(imageView)
|
||||
imageView.set(.width, to: 64)
|
||||
imageView.set(.height, to: 64)
|
||||
imageView.center(in: background)
|
||||
return background
|
||||
imageView.center(in: result)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private func setUpGestureRecognizers() {
|
||||
|
@ -104,7 +142,8 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
|
|||
makeViewDraggable()
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
// MARK: - Interaction
|
||||
|
||||
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
dismiss()
|
||||
guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() } // FIXME: Handle more gracefully
|
||||
|
@ -113,14 +152,16 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
|
|||
|
||||
public func show() {
|
||||
self.alpha = 0.0
|
||||
let window = CurrentAppContext().mainWindow!
|
||||
guard let window: UIWindow = CurrentAppContext().mainWindow else { return }
|
||||
|
||||
window.addSubview(self)
|
||||
left = self.autoPinEdge(toSuperviewEdge: .left)
|
||||
left?.isActive = false
|
||||
right = self.autoPinEdge(toSuperviewEdge: .right)
|
||||
right = self.autoPinEdge(toSuperviewEdge: .right, withInset: Values.smallSpacing)
|
||||
top = self.autoPinEdge(toSuperviewEdge: .top, withInset: topMargin)
|
||||
bottom = self.autoPinEdge(toSuperviewEdge: .bottom, withInset: bottomMargin)
|
||||
bottom?.isActive = false
|
||||
|
||||
UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: {
|
||||
self.alpha = 1.0
|
||||
}, completion: nil)
|
||||
|
@ -129,17 +170,24 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
|
|||
public func dismiss() {
|
||||
UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: {
|
||||
self.alpha = 0.0
|
||||
}, completion: { _ in
|
||||
self.callVC.call.removeRemoteVideoRenderer(self.remoteVideoView)
|
||||
self.callVC.setupStateChangeCallbacks()
|
||||
}, completion: { [weak self] _ in
|
||||
if let remoteVideoView: RTCVideoRenderer = self?.remoteVideoView {
|
||||
self?.callVC.call.removeRemoteVideoRenderer(remoteVideoView)
|
||||
}
|
||||
|
||||
self?.callVC.setupStateChangeCallbacks()
|
||||
MiniCallView.current = nil
|
||||
self.removeFromSuperview()
|
||||
self?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: RTCVideoViewDelegate
|
||||
// MARK: - RTCVideoViewDelegate
|
||||
|
||||
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)
|
||||
self.width?.constant = newSize.width
|
||||
self.height?.constant = newSize.height
|
||||
|
@ -148,25 +196,49 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
|
|||
func persistCurrentPosition(newSize: CGSize) {
|
||||
let currentCenter = self.center
|
||||
|
||||
if currentCenter.x < self.superview!.width() / 2 {
|
||||
if currentCenter.x < ((self.superview?.width() ?? 0) / 2) {
|
||||
left?.isActive = true
|
||||
right?.isActive = false
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
left?.isActive = false
|
||||
right?.isActive = true
|
||||
}
|
||||
|
||||
let willTouchTop = currentCenter.y < newSize.height / 2 + topMargin
|
||||
let willTouchBottom = currentCenter.y + newSize.height / 2 >= self.superview!.height()
|
||||
let willTouchTop: Bool = (currentCenter.y < ((newSize.height / 2) + topMargin))
|
||||
let willTouchBottom: Bool = ((currentCenter.y + (newSize.height / 2)) >= (self.superview?.height() ?? 0))
|
||||
|
||||
if willTouchBottom {
|
||||
top?.isActive = false
|
||||
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?.isActive = true
|
||||
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
|
||||
}
|
||||
|
||||
requestMicrophonePermissionIfNeeded {}
|
||||
Permissions.requestMicrophonePermissionIfNeeded()
|
||||
|
||||
let threadId: String = self.viewModel.threadData.threadId
|
||||
|
||||
|
@ -104,12 +104,34 @@ extension ConversationVC:
|
|||
}
|
||||
|
||||
@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)
|
||||
blockedModal.modalPresentationStyle = .overFullScreen
|
||||
blockedModal.modalTransitionStyle = .crossDissolve
|
||||
present(blockedModal, animated: true, completion: nil)
|
||||
let message = String(
|
||||
format: "modal_blocked_explanation".localized(),
|
||||
self.viewModel.threadData.displayName
|
||||
)
|
||||
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
|
||||
}
|
||||
|
@ -185,7 +207,7 @@ extension ConversationVC:
|
|||
func handleLibraryButtonTapped() {
|
||||
let threadId: String = self.viewModel.threadData.threadId
|
||||
|
||||
requestLibraryPermissionIfNeeded { [weak self] in
|
||||
Permissions.requestLibraryPermissionIfNeeded { [weak self] in
|
||||
DispatchQueue.main.async {
|
||||
let sendMediaNavController = SendMediaNavigationController.showingMediaLibraryFirst(
|
||||
threadId: threadId
|
||||
|
@ -198,9 +220,9 @@ extension ConversationVC:
|
|||
}
|
||||
|
||||
func handleCameraButtonTapped() {
|
||||
guard requestCameraPermissionIfNeeded() else { return }
|
||||
guard Permissions.requestCameraPermissionIfNeeded(presentingViewController: self) else { return }
|
||||
|
||||
requestMicrophonePermissionIfNeeded { }
|
||||
Permissions.requestMicrophonePermissionIfNeeded()
|
||||
|
||||
if AVAudioSession.sharedInstance().recordPermission != .granted {
|
||||
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 cellViewModel.cellType != .textOnlyMessage && cellViewModel.variant == .standardIncoming && !cellViewModel.threadIsTrusted {
|
||||
let modal = DownloadAttachmentModal(profile: cellViewModel.profile)
|
||||
modal.modalPresentationStyle = .overFullScreen
|
||||
modal.modalTransitionStyle = .crossDissolve
|
||||
let message: String = String(
|
||||
format: "modal_download_attachment_explanation".localized(),
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -924,20 +965,20 @@ extension ConversationVC:
|
|||
guard let url: URL = URL(string: urlString) else { return }
|
||||
|
||||
// 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(),
|
||||
message: String(format: "modal_open_url_explanation".localized(), url.absoluteString),
|
||||
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)
|
||||
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
|
||||
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()
|
||||
})
|
||||
|
||||
|
@ -1732,7 +1773,7 @@ extension ConversationVC:
|
|||
|
||||
func startVoiceMessageRecording() {
|
||||
// Request permission if needed
|
||||
requestMicrophonePermissionIfNeeded() { [weak self] in
|
||||
Permissions.requestMicrophonePermissionIfNeeded() { [weak self] in
|
||||
self?.cancelVoiceMessageRecording()
|
||||
}
|
||||
|
||||
|
@ -1854,100 +1895,6 @@ extension ConversationVC:
|
|||
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
|
||||
|
||||
@objc func sendScreenshotNotification() {
|
||||
|
|
|
@ -186,7 +186,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers
|
|||
lazy var blockedBanner: InfoBanner = {
|
||||
let result: InfoBanner = InfoBanner(
|
||||
message: self.viewModel.blockedBannerMessage,
|
||||
backgroundColor: Colors.destructive
|
||||
backgroundColor: .danger
|
||||
)
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(unblock))
|
||||
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
|
||||
|
||||
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 = {
|
||||
let result: UILabel = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.themeTextColor = .textPrimary
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
|
||||
return result
|
||||
|
@ -26,8 +26,8 @@ final class ConversationTitleView: UIView {
|
|||
|
||||
private lazy var subtitleLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .systemFont(ofSize: 13)
|
||||
result.themeTextColor = .textPrimary
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
|
||||
return result
|
||||
|
@ -95,23 +95,37 @@ final class ConversationTitleView: UIView {
|
|||
return
|
||||
}
|
||||
|
||||
// Generate the subtitle
|
||||
let subtitle: NSAttributedString? = {
|
||||
let shouldHaveSubtitle: Bool = (
|
||||
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 {
|
||||
return NSAttributedString(
|
||||
subtitleLabel?.attributedText = NSAttributedString(
|
||||
string: "\u{e067} ",
|
||||
attributes: [
|
||||
.font: UIFont.ows_elegantIconsFont(10),
|
||||
.foregroundColor: Colors.text
|
||||
.foregroundColor: textPrimary
|
||||
]
|
||||
)
|
||||
.appending(string: "Muted")
|
||||
return
|
||||
}
|
||||
guard !onlyNotifyForMentions else {
|
||||
// FIXME: This is going to have issues when swapping between light/dark mode
|
||||
let imageAttachment = NSTextAttachment()
|
||||
let color: UIColor = (isDarkMode ? .white : .black)
|
||||
imageAttachment.image = UIImage(named: "NotifyMentions.png")?.asTintedImage(color: color)
|
||||
imageAttachment.image = UIImage(named: "NotifyMentions.png")?.asTintedImage(color: textPrimary)
|
||||
imageAttachment.bounds = CGRect(
|
||||
x: 0,
|
||||
y: -2,
|
||||
|
@ -119,23 +133,17 @@ final class ConversationTitleView: UIView {
|
|||
height: Values.smallFontSize
|
||||
)
|
||||
|
||||
return NSAttributedString(attachment: imageAttachment)
|
||||
subtitleLabel?.attributedText = NSAttributedString(attachment: imageAttachment)
|
||||
.appending(string: " ")
|
||||
.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")")
|
||||
}()
|
||||
|
||||
self.titleLabel.text = name
|
||||
self.titleLabel.font = .boldSystemFont(
|
||||
ofSize: (subtitle != nil ?
|
||||
Values.mediumFontSize :
|
||||
Values.veryLargeFontSize
|
||||
subtitleLabel?.attributedText = NSAttributedString(
|
||||
string: "\(userCount) member\(userCount == 1 ? "" : "s")"
|
||||
)
|
||||
)
|
||||
self.subtitleLabel.attributedText = subtitle
|
||||
}
|
||||
|
||||
// Contact threads also have the call button to compensate for
|
||||
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 {
|
||||
private let message: String
|
||||
private let snBackgroundColor: UIColor
|
||||
|
||||
init(message: String, backgroundColor: UIColor) {
|
||||
self.message = message
|
||||
self.snBackgroundColor = backgroundColor
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
|
||||
final class InfoBanner: UIView {
|
||||
init(message: String, backgroundColor: ThemeValue) {
|
||||
super.init(frame: CGRect.zero)
|
||||
setUpViewHierarchy()
|
||||
|
||||
setUpViewHierarchy(message: message, backgroundColor: backgroundColor)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
@ -18,16 +18,18 @@ final class InfoBanner : UIView {
|
|||
preconditionFailure("Use init(coder:) instead.")
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy() {
|
||||
backgroundColor = snBackgroundColor
|
||||
let label = UILabel()
|
||||
label.text = message
|
||||
private func setUpViewHierarchy(message: String, backgroundColor: ThemeValue) {
|
||||
themeBackgroundColor = backgroundColor
|
||||
|
||||
let label: UILabel = UILabel()
|
||||
label.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
||||
label.textColor = .white
|
||||
label.numberOfLines = 0
|
||||
label.text = message
|
||||
label.themeTextColor = .textPrimary
|
||||
label.textAlignment = .center
|
||||
label.lineBreakMode = .byWordWrapping
|
||||
label.numberOfLines = 0
|
||||
addSubview(label)
|
||||
|
||||
label.pin(to: self, withInset: Values.mediumSpacing)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,10 +15,10 @@ final class JoinOpenGroupModal: Modal {
|
|||
self.name = (name ?? "Open Group")
|
||||
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.")
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
title: "TXT_DELETE_TITLE".localized()
|
||||
) { [weak self] _, _, completionHandler in
|
||||
let message = (threadViewModel.currentUserIsClosedGroupAdmin == true ?
|
||||
"admin_group_leave_warning".localized() :
|
||||
"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE".localized()
|
||||
let confirmationModal: ConfirmationModal = ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
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(
|
||||
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)
|
||||
self?.present(confirmationModal, animated: true, completion: nil)
|
||||
}
|
||||
delete.themeBackgroundColor = .conversationButton_swipeDestructive
|
||||
|
||||
|
|
|
@ -310,4 +310,26 @@ public class HomeViewModel {
|
|||
public func updateThreadData(_ updatedData: [SectionModel]) {
|
||||
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_seed_title" = "Ihr Wiederherstellungssatz";
|
||||
"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_seed_title" = "Your Recovery Phrase";
|
||||
"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_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_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_seed_title" = "عبارت بازیابی شما";
|
||||
"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_seed_title" = "Palatusvirkkeesi";
|
||||
"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_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_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_seed_title" = "Your Recovery Phrase";
|
||||
"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_seed_title" = "Fraza za oporavak";
|
||||
"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_seed_title" = "Kata pemulihan anda";
|
||||
"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_seed_title" = "Frase di recupero";
|
||||
"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_seed_title" = "あなたのリカバリーフレーズ";
|
||||
"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_seed_title" = "Uw Herstel Zin";
|
||||
"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_seed_title" = "Twoja fraza odzyskiwania";
|
||||
"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_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_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_seed_title" = "Ваша секретная фраза";
|
||||
"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_seed_title" = "Your Recovery Phrase";
|
||||
"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_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_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_seed_title" = "Din Återställningsfras";
|
||||
"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_seed_title" = "วลีกู้คืนของคุณ";
|
||||
"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_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_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_seed_title" = "您的回復用字句";
|
||||
"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_seed_title" = "您的恢复口令";
|
||||
"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,
|
||||
confirmTitle: "continue_2".localized(),
|
||||
confirmStyle: .textPrimary
|
||||
) { _ in requestMicrophonePermissionIfNeeded() }
|
||||
) { _ in Permissions.requestMicrophonePermissionIfNeeded() }
|
||||
)
|
||||
)
|
||||
]
|
||||
|
|
|
@ -21,6 +21,7 @@ public class ConfirmationModal: Modal {
|
|||
|
||||
let title: String
|
||||
let explanation: String?
|
||||
let attributedExplanation: NSAttributedString?
|
||||
let stateToShow: State
|
||||
let confirmTitle: String?
|
||||
let confirmStyle: ThemeValue
|
||||
|
@ -28,22 +29,26 @@ public class ConfirmationModal: Modal {
|
|||
let cancelStyle: ThemeValue
|
||||
let dismissOnConfirm: Bool
|
||||
let onConfirm: ((UIViewController) -> ())?
|
||||
let afterClosed: (() -> ())?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(
|
||||
title: String,
|
||||
explanation: String? = nil,
|
||||
attributedExplanation: NSAttributedString? = nil,
|
||||
stateToShow: State = .always,
|
||||
confirmTitle: String? = nil,
|
||||
confirmStyle: ThemeValue = .textPrimary,
|
||||
cancelTitle: String = "TXT_CANCEL_TITLE".localized(),
|
||||
cancelStyle: ThemeValue = .danger,
|
||||
dismissOnConfirm: Bool = true,
|
||||
onConfirm: ((UIViewController) -> ())? = nil
|
||||
onConfirm: ((UIViewController) -> ())? = nil,
|
||||
afterClosed: (() -> ())? = nil
|
||||
) {
|
||||
self.title = title
|
||||
self.explanation = explanation
|
||||
self.attributedExplanation = attributedExplanation
|
||||
self.stateToShow = stateToShow
|
||||
self.confirmTitle = confirmTitle
|
||||
self.confirmStyle = confirmStyle
|
||||
|
@ -51,11 +56,15 @@ public class ConfirmationModal: Modal {
|
|||
self.cancelStyle = cancelStyle
|
||||
self.dismissOnConfirm = dismissOnConfirm
|
||||
self.onConfirm = onConfirm
|
||||
self.afterClosed = afterClosed
|
||||
}
|
||||
|
||||
// MARK: - Mutation
|
||||
|
||||
public func with(onConfirm: ((UIViewController) -> ())? = nil) -> Info {
|
||||
public func with(
|
||||
onConfirm: ((UIViewController) -> ())? = nil,
|
||||
afterClosed: (() -> ())? = nil
|
||||
) -> Info {
|
||||
return Info(
|
||||
title: self.title,
|
||||
explanation: self.explanation,
|
||||
|
@ -65,7 +74,8 @@ public class ConfirmationModal: Modal {
|
|||
cancelTitle: self.cancelTitle,
|
||||
cancelStyle: self.cancelStyle,
|
||||
dismissOnConfirm: self.dismissOnConfirm,
|
||||
onConfirm: (onConfirm ?? self.onConfirm)
|
||||
onConfirm: (onConfirm ?? self.onConfirm),
|
||||
afterClosed: (afterClosed ?? self.afterClosed)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -75,6 +85,7 @@ public class ConfirmationModal: Modal {
|
|||
return (
|
||||
lhs.title == rhs.title &&
|
||||
lhs.explanation == rhs.explanation &&
|
||||
lhs.attributedExplanation == rhs.attributedExplanation &&
|
||||
lhs.stateToShow == rhs.stateToShow &&
|
||||
lhs.confirmTitle == rhs.confirmTitle &&
|
||||
lhs.confirmStyle == rhs.confirmStyle &&
|
||||
|
@ -87,6 +98,7 @@ public class ConfirmationModal: Modal {
|
|||
public func hash(into hasher: inout Hasher) {
|
||||
title.hash(into: &hasher)
|
||||
explanation.hash(into: &hasher)
|
||||
attributedExplanation.hash(into: &hasher)
|
||||
stateToShow.hash(into: &hasher)
|
||||
confirmTitle.hash(into: &hasher)
|
||||
confirmStyle.hash(into: &hasher)
|
||||
|
@ -174,15 +186,28 @@ public class ConfirmationModal: Modal {
|
|||
viewController.dismiss(animated: true)
|
||||
}
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
super.init(afterClosed: info.afterClosed)
|
||||
|
||||
self.modalPresentationStyle = .overFullScreen
|
||||
self.modalTransitionStyle = .crossDissolve
|
||||
|
||||
// Set the content based on the provided info
|
||||
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.setThemeTitleColor(info.confirmStyle, for: .normal)
|
||||
confirmButton.isHidden = (info.confirmTitle == nil)
|
||||
|
|
|
@ -6,6 +6,8 @@ import SessionUIKit
|
|||
public class Modal: BaseVC, UIGestureRecognizerDelegate {
|
||||
private static let cornerRadius: CGFloat = 11
|
||||
|
||||
private let afterClosed: (() -> ())?
|
||||
|
||||
// MARK: - Components
|
||||
|
||||
lazy var dimmingView: UIView = {
|
||||
|
@ -52,6 +54,16 @@ public class Modal: BaseVC, UIGestureRecognizerDelegate {
|
|||
|
||||
// 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() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
@ -120,7 +132,9 @@ public class Modal: BaseVC, UIGestureRecognizerDelegate {
|
|||
targetViewController = targetViewController?.presentingViewController
|
||||
}
|
||||
|
||||
targetViewController?.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
targetViewController?.presentingViewController?.dismiss(animated: true) { [weak self] in
|
||||
self?.afterClosed?()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIGestureRecognizerDelegate
|
||||
|
|
|
@ -5,96 +5,138 @@ import Photos
|
|||
import PhotosUI
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public 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
|
||||
guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() }
|
||||
presentingVC.present(modal, animated: true, completion: nil)
|
||||
return false
|
||||
|
||||
case .notDetermined:
|
||||
AVCaptureDevice.requestAccess(for: .video, completionHandler: { _ in })
|
||||
return false
|
||||
|
||||
default: return false
|
||||
public enum Permissions {
|
||||
public static func requestCameraPermissionIfNeeded(
|
||||
presentingViewController: UIViewController? = nil
|
||||
) -> Bool {
|
||||
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
||||
case .authorized: return true
|
||||
case .denied, .restricted:
|
||||
guard let presentingViewController: UIViewController = (presentingViewController ?? CurrentAppContext().frontmostViewController()) else { return false }
|
||||
|
||||
let confirmationModal: ConfirmationModal = ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "Session",
|
||||
explanation: String(
|
||||
format: "modal_permission_explanation".localized(),
|
||||
"modal_permission_camera".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)
|
||||
return false
|
||||
|
||||
case .notDetermined:
|
||||
AVCaptureDevice.requestAccess(for: .video, completionHandler: { _ in })
|
||||
return false
|
||||
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func requestMicrophonePermissionIfNeeded(onNotGranted: (() -> Void)? = nil) {
|
||||
switch AVAudioSession.sharedInstance().recordPermission {
|
||||
case .granted: break
|
||||
case .denied:
|
||||
onNotGranted?()
|
||||
let modal = PermissionMissingModal(permission: "microphone") {
|
||||
public static func requestMicrophonePermissionIfNeeded(
|
||||
presentingViewController: UIViewController? = nil,
|
||||
onNotGranted: (() -> Void)? = nil
|
||||
) {
|
||||
switch AVAudioSession.sharedInstance().recordPermission {
|
||||
case .granted: break
|
||||
case .denied:
|
||||
guard let presentingViewController: UIViewController = (presentingViewController ?? CurrentAppContext().frontmostViewController()) else { return }
|
||||
onNotGranted?()
|
||||
}
|
||||
modal.modalPresentationStyle = .overFullScreen
|
||||
modal.modalTransitionStyle = .crossDissolve
|
||||
guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() }
|
||||
presentingVC.present(modal, animated: true, completion: nil)
|
||||
|
||||
case .undetermined:
|
||||
onNotGranted?()
|
||||
AVAudioSession.sharedInstance().requestRecordPermission { _ in }
|
||||
|
||||
default: break
|
||||
|
||||
let confirmationModal: ConfirmationModal = ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "Session",
|
||||
explanation: String(
|
||||
format: "modal_permission_explanation".localized(),
|
||||
"modal_permission_microphone".localized()
|
||||
),
|
||||
confirmTitle: "modal_permission_settings_title".localized(),
|
||||
dismissOnConfirm: false,
|
||||
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) {
|
||||
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()
|
||||
public static func requestLibraryPermissionIfNeeded(
|
||||
presentingViewController: UIViewController? = nil,
|
||||
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
|
||||
|
||||
PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
|
||||
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()
|
||||
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
|
||||
guard let presentingVC = CurrentAppContext().frontmostViewController() else { preconditionFailure() }
|
||||
presentingVC.present(modal, animated: true, completion: nil)
|
||||
|
||||
default: return
|
||||
|
||||
switch authorizationStatus {
|
||||
case .authorized, .limited: onAuthorized()
|
||||
case .denied, .restricted:
|
||||
guard let presentingViewController: UIViewController = (presentingViewController ?? CurrentAppContext().frontmostViewController()) else { return }
|
||||
|
||||
let confirmationModal: ConfirmationModal = ConfirmationModal(
|
||||
info: ConfirmationModal.Info(
|
||||
title: "Session",
|
||||
explanation: String(
|
||||
format: "modal_permission_explanation".localized(),
|
||||
"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 SessionUIKit
|
||||
|
||||
public extension Notification.Name {
|
||||
static let windowSubviewsChanged = Notification.Name("windowSubviewsChanged")
|
||||
}
|
||||
|
||||
|
||||
public class TraitObservingWindow: UIWindow {
|
||||
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.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.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
|
||||
extension UIView {
|
||||
|
||||
func makeViewDraggable() {
|
||||
|
@ -8,29 +11,36 @@ extension UIView {
|
|||
}
|
||||
|
||||
@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 {
|
||||
draggedView.center = location
|
||||
|
||||
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: {
|
||||
draggedView.center.x = self.superview!.layer.frame.width - draggedView.width() / 2
|
||||
}, completion: nil)
|
||||
}else{
|
||||
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseIn, animations: {
|
||||
draggedView.center.x = draggedView.width() / 2
|
||||
draggedView.center.x = (superview.layer.frame.width - (draggedView.width() / 2) - Values.smallSpacing)
|
||||
}, 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 {
|
||||
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)
|
||||
}
|
||||
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: {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,6 +88,10 @@ internal enum Theme_ClassicDark: ThemeColors {
|
|||
.conversationButton_unreadBubbleText: .classicDark6,
|
||||
.conversationButton_swipeDestructive: .dangerDark,
|
||||
.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_swipeDestructive: .dangerLight,
|
||||
.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_swipeDestructive: .dangerDark,
|
||||
.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_swipeDestructive: .dangerLight,
|
||||
.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_swipeSecondary
|
||||
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 {
|
||||
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
|
||||
// https://github.com/apple/swift/blob/main/stdlib/public/core/String.swift#L901)
|
||||
|
|
Loading…
Reference in New Issue