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

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

View File

@ -111,7 +111,6 @@
7B0EFDF4275490EA00FFAAE7 /* ringing.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 7B0EFDF3275490EA00FFAAE7 /* ringing.mp3 */; };
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 */,

View File

@ -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

View File

@ -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() {

View File

@ -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)
}

View File

@ -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() {

View File

@ -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)
}
}

View File

@ -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() {

View File

@ -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)

View File

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

View File

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

View File

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

View File

@ -17,8 +17,8 @@ final class ConversationTitleView: UIView {
private lazy var titleLabel: UILabel = {
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 = (

View File

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

View File

@ -1,13 +1,13 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
final class InfoBanner : UIView {
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)
}
}

View File

@ -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.")
}

View File

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

View File

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

View File

@ -547,47 +547,31 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve
style: .destructive,
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

View File

@ -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)
}
}
}

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

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

View File

@ -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)

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
]
}

View File

@ -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
]
}

View File

@ -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
]
}

View File

@ -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
]
}

View File

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

View File

@ -23,6 +23,13 @@ public extension NSAttributedString {
func appending(string: String, attributes: [Key: Any]? = nil) -> NSAttributedString {
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)