Merge pull request #313 from loki-project/refactor-4
Refactoring Part 3
This commit is contained in:
commit
60aa2811bc
20
Podfile
20
Podfile
|
@ -15,7 +15,7 @@ target 'Session' do
|
|||
pod 'Sodium', :inhibit_warnings => true
|
||||
pod 'SSZipArchive', :inhibit_warnings => true
|
||||
pod 'Starscream', git: 'https://github.com/signalapp/Starscream.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/signalapp/YapDatabase.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
pod 'YYImage', git: 'https://github.com/signalapp/YYImage', :inhibit_warnings => true
|
||||
pod 'ZXingObjC', :inhibit_warnings => true
|
||||
end
|
||||
|
@ -28,7 +28,7 @@ target 'SessionShareExtension' do
|
|||
pod 'PromiseKit', :inhibit_warnings => true
|
||||
pod 'PureLayout', '~> 3.1.4', :inhibit_warnings => true
|
||||
pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/signalapp/YapDatabase.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
end
|
||||
|
||||
target 'SessionPushNotificationExtension' do
|
||||
|
@ -39,7 +39,7 @@ target 'SessionPushNotificationExtension' do
|
|||
pod 'PromiseKit', :inhibit_warnings => true
|
||||
pod 'PureLayout', '~> 3.1.4', :inhibit_warnings => true
|
||||
pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/signalapp/YapDatabase.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
end
|
||||
|
||||
target 'SignalUtilitiesKit' do
|
||||
|
@ -57,7 +57,7 @@ target 'SignalUtilitiesKit' do
|
|||
pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true
|
||||
pod 'Starscream', git: 'https://github.com/signalapp/Starscream.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
pod 'SwiftProtobuf', '~> 1.5.0', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/signalapp/YapDatabase.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
pod 'YYImage', git: 'https://github.com/signalapp/YYImage', :inhibit_warnings => true
|
||||
end
|
||||
|
||||
|
@ -69,9 +69,15 @@ target 'SessionMessagingKit' do
|
|||
pod 'AFNetworking', inhibit_warnings: true
|
||||
pod 'CryptoSwift', :inhibit_warnings => true
|
||||
pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true
|
||||
pod 'HKDFKit', :inhibit_warnings => true
|
||||
pod 'Mantle', git: 'https://github.com/signalapp/Mantle', branch: 'signal-master', :inhibit_warnings => true
|
||||
pod 'PromiseKit', :inhibit_warnings => true
|
||||
pod 'PureLayout', '~> 3.1.4', :inhibit_warnings => true
|
||||
pod 'Reachability', :inhibit_warnings => true
|
||||
pod 'SAMKeychain', :inhibit_warnings => true
|
||||
pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true
|
||||
pod 'SwiftProtobuf', '~> 1.5.0', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
end
|
||||
|
||||
target 'SessionProtocolKit' do
|
||||
|
@ -90,13 +96,19 @@ target 'SessionSnodeKit' do
|
|||
pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true
|
||||
pod 'PromiseKit', :inhibit_warnings => true
|
||||
pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
end
|
||||
|
||||
target 'SessionUtilitiesKit' do
|
||||
pod 'AFNetworking', inhibit_warnings: true
|
||||
pod 'CryptoSwift', :inhibit_warnings => true
|
||||
pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true
|
||||
pod 'Mantle', git: 'https://github.com/signalapp/Mantle', branch: 'signal-master', :inhibit_warnings => true
|
||||
pod 'PromiseKit', :inhibit_warnings => true
|
||||
pod 'PureLayout', '~> 3.1.4', :inhibit_warnings => true
|
||||
pod 'SAMKeychain', :inhibit_warnings => true
|
||||
pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
|
|
10
Podfile.lock
10
Podfile.lock
|
@ -144,7 +144,7 @@ DEPENDENCIES:
|
|||
- SSZipArchive
|
||||
- Starscream (from `https://github.com/signalapp/Starscream.git`, branch `signal-release`)
|
||||
- SwiftProtobuf (~> 1.5.0)
|
||||
- YapDatabase/SQLCipher (from `https://github.com/signalapp/YapDatabase.git`, branch `signal-release`)
|
||||
- YapDatabase/SQLCipher (from `https://github.com/loki-project/session-ios-yap-database.git`, branch `signal-release`)
|
||||
- YYImage (from `https://github.com/signalapp/YYImage`)
|
||||
- ZXingObjC
|
||||
|
||||
|
@ -181,7 +181,7 @@ EXTERNAL SOURCES:
|
|||
:git: https://github.com/signalapp/Starscream.git
|
||||
YapDatabase:
|
||||
:branch: signal-release
|
||||
:git: https://github.com/signalapp/YapDatabase.git
|
||||
:git: https://github.com/loki-project/session-ios-yap-database.git
|
||||
YYImage:
|
||||
:git: https://github.com/signalapp/YYImage
|
||||
|
||||
|
@ -199,8 +199,8 @@ CHECKOUT OPTIONS:
|
|||
:commit: b09ea163c3cb305152c65b299cb024610f52e735
|
||||
:git: https://github.com/signalapp/Starscream.git
|
||||
YapDatabase:
|
||||
:commit: e43ab163b2dcb4c817339c819b07dac545f05fea
|
||||
:git: https://github.com/signalapp/YapDatabase.git
|
||||
:commit: 5806f6b6e0b34124ee09283a9eca9ce7e6eaf14e
|
||||
:git: https://github.com/loki-project/session-ios-yap-database.git
|
||||
YYImage:
|
||||
:commit: d91910e6f313a255febbf69795198e74259bd51c
|
||||
:git: https://github.com/signalapp/YYImage
|
||||
|
@ -230,6 +230,6 @@ SPEC CHECKSUMS:
|
|||
YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: 3489ed70ea51f2bf705bf99703efc71d697de373
|
||||
PODFILE CHECKSUM: 4ebb6a2d7cbbdd42f72f7e7669db8aff6e26cf7c
|
||||
|
||||
COCOAPODS: 1.10.0.rc.1
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
|
||||
extension AppDelegate : OpenGroupAPIDelegate {
|
||||
|
||||
public func updateProfileIfNeeded(for channel: UInt64, on server: String, from info: OpenGroupInfo) {
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
let publicChatID = "\(server).\(channel)"
|
||||
Storage.writeSync { transaction in
|
||||
// Update user count
|
||||
storage.setUserCount(info.memberCount, forPublicChatWithID: publicChatID, in: transaction)
|
||||
let groupThread = TSGroupThread.getOrCreateThread(withGroupId: publicChatID.data(using: .utf8)!, groupType: .openGroup, transaction: transaction)
|
||||
// Update display name if needed
|
||||
let groupModel = groupThread.groupModel
|
||||
if groupModel.groupName != info.displayName {
|
||||
let newGroupModel = TSGroupModel(title: info.displayName, memberIds: groupModel.groupMemberIds, image: groupModel.groupImage, groupId: groupModel.groupId, groupType: groupModel.groupType, adminIds: groupModel.groupAdminIds)
|
||||
groupThread.groupModel = newGroupModel
|
||||
groupThread.save(with: transaction)
|
||||
}
|
||||
// Download and update profile picture if needed
|
||||
let oldProfilePictureURL = storage.getProfilePictureURL(forPublicChatWithID: publicChatID, in: transaction)
|
||||
if oldProfilePictureURL != info.profilePictureURL || groupModel.groupImage == nil {
|
||||
storage.setProfilePictureURL(info.profilePictureURL, forPublicChatWithID: publicChatID, in: transaction)
|
||||
if let profilePictureURL = info.profilePictureURL {
|
||||
var sanitizedServerURL = server
|
||||
var sanitizedProfilePictureURL = profilePictureURL
|
||||
while sanitizedServerURL.hasSuffix("/") { sanitizedServerURL.removeLast(1) }
|
||||
while sanitizedProfilePictureURL.hasPrefix("/") { sanitizedProfilePictureURL.removeFirst(1) }
|
||||
let url = "\(sanitizedServerURL)/\(sanitizedProfilePictureURL)"
|
||||
FileServerAPI.downloadAttachment(from: url).map2 { data in
|
||||
let attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(data.count), sourceFilename: nil, caption: nil, albumMessageId: nil)
|
||||
try attachmentStream.write(data)
|
||||
groupThread.updateAvatar(with: attachmentStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
|
||||
extension AppDelegate : SharedSenderKeysDelegate {
|
||||
|
||||
public func requestSenderKey(for groupPublicKey: String, senderPublicKey: String, using transaction: Any) {
|
||||
ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction as! YapDatabaseReadWriteTransaction)
|
||||
}
|
||||
}
|
|
@ -132,8 +132,7 @@ final class ConversationCell : UITableViewCell {
|
|||
// MARK: Updating
|
||||
private func update() {
|
||||
AssertIsOnMainThread()
|
||||
let thread = threadViewModel.threadRecord
|
||||
guard let threadID = thread.uniqueId else { return }
|
||||
guard let thread = threadViewModel?.threadRecord, let threadID = thread.uniqueId else { return }
|
||||
MentionsManager.populateUserPublicKeyCacheIfNeeded(for: threadID) // FIXME: This is a terrible place to do this
|
||||
let isBlocked: Bool
|
||||
if let thread = thread as? TSContactThread {
|
||||
|
@ -166,7 +165,7 @@ final class ConversationCell : UITableViewCell {
|
|||
let image: UIImage
|
||||
let status = MessageRecipientStatusUtils.recipientStatus(outgoingMessage: lastMessage)
|
||||
switch status {
|
||||
case .calculatingPoW, .uploading, .sending: image = #imageLiteral(resourceName: "CircleDotDotDot").asTintedImage(color: Colors.text)!
|
||||
case .uploading, .sending: image = #imageLiteral(resourceName: "CircleDotDotDot").asTintedImage(color: Colors.text)!
|
||||
case .sent, .skipped, .delivered: image = #imageLiteral(resourceName: "CircleCheck").asTintedImage(color: Colors.text)!
|
||||
case .read:
|
||||
statusIndicatorView.backgroundColor = isLightMode ? .black : .white
|
||||
|
|
|
@ -50,11 +50,11 @@ final class ConversationTitleView : UIView {
|
|||
updateSubtitleForCurrentStatus()
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(handleProfileChangedNotification(_:)), name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange), object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleCalculatingPoWNotification(_:)), name: .calculatingPoW, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleRoutingNotification(_:)), name: .routing, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleCalculatingMessagePoWNotification(_:)), name: .calculatingMessagePoW, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleEncryptingMessageNotification(_:)), name: .encryptingMessage, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageSendingNotification(_:)), name: .messageSending, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageSentNotification(_:)), name: .messageSent, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageFailedNotification(_:)), name: .messageFailed, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageSendingFailedNotification(_:)), name: .messageSendingFailed, object: nil)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
@ -112,12 +112,12 @@ final class ConversationTitleView : UIView {
|
|||
updateProfilePicture()
|
||||
}
|
||||
|
||||
@objc private func handleCalculatingPoWNotification(_ notification: Notification) {
|
||||
@objc private func handleCalculatingMessagePoWNotification(_ notification: Notification) {
|
||||
guard let timestamp = notification.object as? NSNumber else { return }
|
||||
setStatusIfNeeded(to: .calculatingPoW, forMessageWithTimestamp: timestamp)
|
||||
}
|
||||
|
||||
@objc private func handleRoutingNotification(_ notification: Notification) {
|
||||
@objc private func handleEncryptingMessageNotification(_ notification: Notification) {
|
||||
guard let timestamp = notification.object as? NSNumber else { return }
|
||||
setStatusIfNeeded(to: .routing, forMessageWithTimestamp: timestamp)
|
||||
}
|
||||
|
@ -136,7 +136,7 @@ final class ConversationTitleView : UIView {
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func handleMessageFailedNotification(_ notification: Notification) {
|
||||
@objc private func handleMessageSendingFailedNotification(_ notification: Notification) {
|
||||
guard let timestamp = notification.object as? NSNumber else { return }
|
||||
clearStatusIfNeededForMessageWithTimestamp(timestamp)
|
||||
}
|
||||
|
@ -149,14 +149,7 @@ final class ConversationTitleView : UIView {
|
|||
uncheckedTargetInteraction = interaction
|
||||
}
|
||||
guard let targetInteraction = uncheckedTargetInteraction, targetInteraction.interactionType() == .outgoingMessage,
|
||||
status.rawValue > (currentStatus?.rawValue ?? 0), let hexEncodedPublicKey = targetInteraction.thread.contactIdentifier() else { return }
|
||||
var masterHexEncodedPublicKey: String!
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
storage.dbReadConnection.read { transaction in
|
||||
masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) ?? hexEncodedPublicKey
|
||||
}
|
||||
let isSlaveDevice = masterHexEncodedPublicKey != hexEncodedPublicKey
|
||||
guard !isSlaveDevice else { return }
|
||||
status.rawValue > (currentStatus?.rawValue ?? 0) else { return }
|
||||
currentStatus = status
|
||||
}
|
||||
|
||||
|
@ -174,7 +167,8 @@ final class ConversationTitleView : UIView {
|
|||
}
|
||||
|
||||
@objc func updateSubtitleForCurrentStatus() {
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.subtitleLabel.isHidden = false
|
||||
let subtitle = NSMutableAttributedString()
|
||||
if let muteEndDate = self.thread.mutedUntilDate, self.thread.isMuted {
|
||||
|
@ -184,16 +178,13 @@ final class ConversationTitleView : UIView {
|
|||
dateFormatter.timeStyle = .medium
|
||||
dateFormatter.dateStyle = .medium
|
||||
subtitle.append(NSAttributedString(string: "Muted until " + dateFormatter.string(from: muteEndDate)))
|
||||
} else if let thread = self.thread as? TSGroupThread, !thread.isRSSFeed {
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
} else if let thread = self.thread as? TSGroupThread {
|
||||
var userCount: Int?
|
||||
if thread.groupModel.groupType == .closedGroup {
|
||||
userCount = GroupUtilities.getClosedGroupMemberCount(thread)
|
||||
} else if thread.groupModel.groupType == .openGroup {
|
||||
storage.dbReadConnection.read { transaction in
|
||||
if let publicChat = LokiDatabaseUtilities.getPublicChat(for: self.thread.uniqueId!, in: transaction) {
|
||||
userCount = storage.getUserCount(for: publicChat, in: transaction)
|
||||
}
|
||||
if let openGroup = Storage.shared.getOpenGroup(for: self.thread.uniqueId!) {
|
||||
userCount = Storage.shared.getUserCount(forOpenGroupWithID: openGroup.id)
|
||||
}
|
||||
}
|
||||
if let userCount = userCount {
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
|
||||
// MARK: - User Selection View
|
||||
|
||||
@objc(LKMentionCandidateSelectionView)
|
||||
final class MentionCandidateSelectionView : UIView, UITableViewDataSource, UITableViewDelegate {
|
||||
@objc var mentionCandidates: [Mention] = [] { didSet { tableView.reloadData() } }
|
||||
|
@ -173,7 +171,8 @@ private extension MentionCandidateSelectionView {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: Delegate
|
||||
// MARK: - Delegate
|
||||
|
||||
@objc(LKMentionCandidateSelectionViewDelegate)
|
||||
protocol MentionCandidateSelectionViewDelegate {
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ final class OptionView : UIView {
|
|||
// Set up shadow
|
||||
layer.shadowColor = UIColor.black.cgColor
|
||||
layer.shadowOffset = CGSize(width: 0, height: 0.8)
|
||||
layer.shadowOpacity = isLightMode ? 0.4 : 1
|
||||
layer.shadowOpacity = isLightMode ? 0.16 : 1
|
||||
layer.shadowRadius = isLightMode ? 4 : 6
|
||||
// Set up title label
|
||||
let titleLabel = UILabel()
|
||||
|
|
|
@ -18,7 +18,7 @@ final class PathStatusView : UIView {
|
|||
layer.cornerRadius = Values.pathStatusViewSize / 2
|
||||
layer.masksToBounds = false
|
||||
if OnionRequestAPI.paths.isEmpty {
|
||||
OnionRequestAPI.paths = Storage.getOnionRequestPaths()
|
||||
OnionRequestAPI.paths = Storage.shared.getOnionRequestPaths()
|
||||
}
|
||||
let color = (!OnionRequestAPI.paths.isEmpty) ? Colors.accent : Colors.pathsBuilding
|
||||
setColor(to: color, isAnimated: false)
|
||||
|
|
|
@ -65,9 +65,9 @@ final class VoiceMessageView : UIView {
|
|||
setUpViewHierarchy()
|
||||
if voiceMessage.isDownloaded {
|
||||
guard let url = (voiceMessage as? TSAttachmentStream)?.originalMediaURL else {
|
||||
return print("[Loki] Couldn't get URL for voice message.")
|
||||
return SNLog("Couldn't get URL for voice message.")
|
||||
}
|
||||
if let cachedVolumeSamples = Storage.getVolumeSamples(for: voiceMessage.uniqueId!), cachedVolumeSamples.count == targetSampleCount {
|
||||
if let cachedVolumeSamples = Storage.shared.getVolumeSamples(for: voiceMessage.uniqueId!), cachedVolumeSamples.count == targetSampleCount {
|
||||
self.hideLoader()
|
||||
self.volumeSamples = cachedVolumeSamples
|
||||
} else {
|
||||
|
@ -78,10 +78,10 @@ final class VoiceMessageView : UIView {
|
|||
self.isForcedAnimation = true
|
||||
self.volumeSamples = volumeSamples
|
||||
Storage.write { transaction in
|
||||
Storage.setVolumeSamples(for: voiceMessageID, to: volumeSamples, using: transaction)
|
||||
Storage.shared.setVolumeSamples(for: voiceMessageID, to: volumeSamples, using: transaction)
|
||||
}
|
||||
}.catch(on: DispatchQueue.main) { error in
|
||||
print("[Loki] Couldn't sample audio file due to error: \(error).")
|
||||
SNLog("Couldn't sample audio file due to error: \(error).")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -2,24 +2,21 @@ import SessionMessagingKit
|
|||
import SessionProtocolKit
|
||||
import SessionSnodeKit
|
||||
|
||||
extension OWSPrimaryStorage : OWSPrimaryStorageProtocol { }
|
||||
|
||||
@objc(SNConfiguration)
|
||||
final class Configuration : NSObject {
|
||||
|
||||
private static let pnServerURL = "https://live.apns.getsession.org"
|
||||
private static let pnServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
|
||||
|
||||
@objc static func performMainSetup() {
|
||||
SNMessagingKit.configure(
|
||||
storage: Storage.shared,
|
||||
signalStorage: OWSPrimaryStorage.shared(),
|
||||
identityKeyStore: OWSIdentityManager.shared(),
|
||||
sessionRestorationImplementation: SessionRestorationImplementation(),
|
||||
certificateValidator: SMKCertificateDefaultValidator(trustRoot: OWSUDManagerImpl.trustRoot()),
|
||||
openGroupAPIDelegate: UIApplication.shared.delegate as! AppDelegate,
|
||||
pnServerURL: pnServerURL,
|
||||
pnServerPublicKey: pnServerURL
|
||||
certificateValidator: SMKCertificateDefaultValidator(trustRoot: OWSUDManagerImpl.trustRoot())
|
||||
)
|
||||
SessionProtocolKit.configure(storage: Storage.shared, sharedSenderKeysDelegate: UIApplication.shared.delegate as! AppDelegate)
|
||||
SessionProtocolKit.configure(storage: Storage.shared, sharedSenderKeysDelegate: MessageSender.shared)
|
||||
SessionSnodeKit.configure(storage: Storage.shared)
|
||||
SessionUtilitiesKit.configure(owsPrimaryStorage: OWSPrimaryStorage.shared(), maxFileSize: UInt(Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
|
||||
extension Storage : SessionProtocolKitStorageProtocol {
|
||||
|
||||
private func getClosedGroupRatchetCollection(_ collection: ClosedGroupRatchetCollectionType, for groupPublicKey: String) -> String {
|
||||
switch collection {
|
||||
case .old: return "LokiOldClosedGroupRatchetCollection.\(groupPublicKey)"
|
||||
case .current: return "LokiClosedGroupRatchetCollection.\(groupPublicKey)"
|
||||
}
|
||||
}
|
||||
|
||||
public func getClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current) -> ClosedGroupRatchet? {
|
||||
let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey)
|
||||
var result: ClosedGroupRatchet?
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: senderPublicKey, inCollection: collection) as? ClosedGroupRatchet
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet, in collection: ClosedGroupRatchetCollectionType = .current, using transaction: Any) {
|
||||
let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey)
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(ratchet, forKey: senderPublicKey, inCollection: collection)
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
extension Storage {
|
||||
|
||||
public static let shared = Storage()
|
||||
|
||||
public func with(_ work: @escaping (Any) -> Void) {
|
||||
Storage.writeSync { work($0) }
|
||||
}
|
||||
|
||||
public func withAsync(_ work: @escaping (Any) -> Void, completion: @escaping () -> Void) {
|
||||
Storage.write(with: { work($0) }, completion: completion)
|
||||
}
|
||||
|
||||
public func getUserPublicKey() -> String? {
|
||||
return OWSIdentityManager.shared().identityKeyPair()?.publicKey.toHexString()
|
||||
}
|
||||
|
||||
public func getUserKeyPair() -> ECKeyPair? {
|
||||
return OWSIdentityManager.shared().identityKeyPair()
|
||||
}
|
||||
|
||||
public func getUserDisplayName() -> String? { fatalError() }
|
||||
}
|
|
@ -1,17 +1,17 @@
|
|||
|
||||
extension Storage {
|
||||
|
||||
static let volumeSamplesCollection = "LokiVolumeSamplesCollection"
|
||||
private static let volumeSamplesCollection = "LokiVolumeSamplesCollection"
|
||||
|
||||
static func getVolumeSamples(for attachment: String) -> [Float]? {
|
||||
public func getVolumeSamples(for attachment: String) -> [Float]? {
|
||||
var result: [Float]?
|
||||
read { transaction in
|
||||
result = transaction.object(forKey: attachment, inCollection: volumeSamplesCollection) as? [Float]
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: attachment, inCollection: Storage.volumeSamplesCollection) as? [Float]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static func setVolumeSamples(for attachment: String, to volumeSamples: [Float], using transaction: YapDatabaseReadWriteTransaction) {
|
||||
transaction.setObject(volumeSamples, forKey: attachment, inCollection: volumeSamplesCollection)
|
||||
public func setVolumeSamples(for attachment: String, to volumeSamples: [Float], using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(volumeSamples, forKey: attachment, inCollection: Storage.volumeSamplesCollection)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,10 +16,8 @@
|
|||
#import "ConversationViewCell.h"
|
||||
#import "ConversationViewItem.h"
|
||||
#import "DateUtil.h"
|
||||
#import "FingerprintViewController.h"
|
||||
#import "MediaDetailViewController.h"
|
||||
#import "NotificationSettingsViewController.h"
|
||||
#import "OWSAddToContactViewController.h"
|
||||
#import "OWSAnyTouchGestureRecognizer.h"
|
||||
#import "OWSAudioPlayer.h"
|
||||
#import "OWSBackup.h"
|
||||
|
@ -35,13 +33,11 @@
|
|||
#import "OWSQuotedMessageView.h"
|
||||
#import "OWSSessionResetJobRecord.h"
|
||||
#import "OWSWindowManager.h"
|
||||
#import "PinEntryView.h"
|
||||
#import "PrivacySettingsTableViewController.h"
|
||||
#import "RemoteVideoView.h"
|
||||
#import "OWSQRCodeScanningViewController.h"
|
||||
#import "SignalApp.h"
|
||||
#import "UIViewController+Permissions.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
#import <SessionProtocolKit/NSData+keyVersionByte.h>
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import <Reachability/Reachability.h>
|
||||
|
@ -53,73 +49,50 @@
|
|||
#import <SignalCoreKit/Threading.h>
|
||||
#import <SignalUtilitiesKit/AttachmentSharing.h>
|
||||
#import <SignalUtilitiesKit/ContactTableViewCell.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSAudioPlayer.h>
|
||||
#import <SignalUtilitiesKit/OWSContactAvatarBuilder.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SessionMessagingKit/OWSAudioPlayer.h>
|
||||
#import <SignalUtilitiesKit/OWSFormat.h>
|
||||
#import <SignalUtilitiesKit/OWSPreferences.h>
|
||||
#import <SessionMessagingKit/OWSPreferences.h>
|
||||
#import <SignalUtilitiesKit/OWSProfileManager.h>
|
||||
#import <SignalUtilitiesKit/OWSQuotedReplyModel.h>
|
||||
#import <SignalUtilitiesKit/OWSSounds.h>
|
||||
#import <SessionMessagingKit/OWSQuotedReplyModel.h>
|
||||
#import <SessionMessagingKit/OWSSounds.h>
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
#import <SignalUtilitiesKit/ThreadUtil.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIFont+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIViewController+OWS.h>
|
||||
#import <SignalUtilitiesKit/AppVersion.h>
|
||||
#import <SignalUtilitiesKit/Contact.h>
|
||||
#import <SignalUtilitiesKit/ContactsUpdater.h>
|
||||
#import <SignalUtilitiesKit/DataSource.h>
|
||||
#import <SignalUtilitiesKit/MIMETypeUtil.h>
|
||||
#import <SignalUtilitiesKit/NSData+Image.h>
|
||||
#import <SignalUtilitiesKit/NSNotificationCenter+OWS.h>
|
||||
#import <SignalUtilitiesKit/NSString+SSK.h>
|
||||
#import <SignalUtilitiesKit/NSTimer+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSAnalytics.h>
|
||||
#import <SignalUtilitiesKit/OWSAnalyticsEvents.h>
|
||||
#import <SignalUtilitiesKit/OWSBackgroundTask.h>
|
||||
#import <SignalUtilitiesKit/OWSCallMessageHandler.h>
|
||||
#import <SessionUtilitiesKit/DataSource.h>
|
||||
#import <SessionUtilitiesKit/MIMETypeUtil.h>
|
||||
#import <SessionUtilitiesKit/NSData+Image.h>
|
||||
#import <SessionUtilitiesKit/NSNotificationCenter+OWS.h>
|
||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||
#import <SessionMessagingKit/OWSBackgroundTask.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsOutputStream.h>
|
||||
#import <SignalUtilitiesKit/OWSDispatch.h>
|
||||
#import <SignalUtilitiesKit/OWSEndSessionMessage.h>
|
||||
#import <SignalUtilitiesKit/LKDeviceLinkMessage.h>
|
||||
#import <SignalUtilitiesKit/OWSError.h>
|
||||
#import <SignalUtilitiesKit/OWSFileSystem.h>
|
||||
#import <SignalUtilitiesKit/OWSIdentityManager.h>
|
||||
#import <SignalUtilitiesKit/OWSMediaGalleryFinder.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageManager.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageReceiver.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageSender.h>
|
||||
#import <SignalUtilitiesKit/OWSOutgoingCallMessage.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+Calling.h>
|
||||
#import <SessionUtilitiesKit/OWSFileSystem.h>
|
||||
#import <SessionMessagingKit/OWSIdentityManager.h>
|
||||
#import <SessionMessagingKit/OWSMediaGalleryFinder.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+SessionStore.h>
|
||||
#import <SignalUtilitiesKit/OWSProfileKeyMessage.h>
|
||||
#import <SignalUtilitiesKit/OWSRecipientIdentity.h>
|
||||
#import <SignalUtilitiesKit/OWSRequestFactory.h>
|
||||
#import <SignalUtilitiesKit/OWSSignalService.h>
|
||||
#import <SignalUtilitiesKit/PhoneNumber.h>
|
||||
#import <SessionMessagingKit/OWSRecipientIdentity.h>
|
||||
#import <SignalUtilitiesKit/SignalAccount.h>
|
||||
#import <SignalUtilitiesKit/SignalRecipient.h>
|
||||
#import <SignalUtilitiesKit/TSAccountManager.h>
|
||||
#import <SignalUtilitiesKit/TSAttachment.h>
|
||||
#import <SignalUtilitiesKit/TSAttachmentPointer.h>
|
||||
#import <SignalUtilitiesKit/TSAttachmentStream.h>
|
||||
#import <SignalUtilitiesKit/TSCall.h>
|
||||
#import <SignalUtilitiesKit/TSContactThread.h>
|
||||
#import <SignalUtilitiesKit/TSErrorMessage.h>
|
||||
#import <SignalUtilitiesKit/TSGroupThread.h>
|
||||
#import <SignalUtilitiesKit/TSIncomingMessage.h>
|
||||
#import <SignalUtilitiesKit/TSInfoMessage.h>
|
||||
#import <SignalUtilitiesKit/TSNetworkManager.h>
|
||||
#import <SignalUtilitiesKit/TSOutgoingMessage.h>
|
||||
#import <SessionMessagingKit/SignalRecipient.h>
|
||||
#import <SessionMessagingKit/TSAccountManager.h>
|
||||
#import <SessionMessagingKit/TSAttachment.h>
|
||||
#import <SessionMessagingKit/TSAttachmentPointer.h>
|
||||
#import <SessionMessagingKit/TSAttachmentStream.h>
|
||||
#import <SessionMessagingKit/TSContactThread.h>
|
||||
#import <SessionMessagingKit/TSErrorMessage.h>
|
||||
#import <SessionMessagingKit/TSGroupThread.h>
|
||||
#import <SessionMessagingKit/TSIncomingMessage.h>
|
||||
#import <SessionMessagingKit/TSInfoMessage.h>
|
||||
#import <SessionMessagingKit/TSOutgoingMessage.h>
|
||||
#import <SignalUtilitiesKit/TSPreKeyManager.h>
|
||||
#import <SignalUtilitiesKit/TSSocketManager.h>
|
||||
#import <SignalUtilitiesKit/TSThread.h>
|
||||
#import <SignalUtilitiesKit/LKGroupUtilities.h>
|
||||
#import <SignalUtilitiesKit/UIImage+OWS.h>
|
||||
#import <SessionMessagingKit/TSThread.h>
|
||||
#import <SessionUtilitiesKit/LKGroupUtilities.h>
|
||||
#import <SessionUtilitiesKit/UIImage+OWS.h>
|
||||
#import <WebRTC/RTCAudioSession.h>
|
||||
#import <WebRTC/RTCCameraPreviewView.h>
|
||||
#import <YYImage/YYImage.h>
|
||||
|
|
|
@ -18,8 +18,7 @@
|
|||
#import <SignalCoreKit/NSObject+OWS.h>
|
||||
#import <SignalCoreKit/OWSAsserts.h>
|
||||
#import <SignalUtilitiesKit/SSKAsserts.h>
|
||||
#import <SignalUtilitiesKit/OWSAnalytics.h>
|
||||
#import <SignalUtilitiesKit/NSArray+Functional.h>
|
||||
#import <SessionUtilitiesKit/NSArray+Functional.h>
|
||||
#import <SignalUtilitiesKit/NSSet+Functional.h>
|
||||
#import <SignalUtilitiesKit/NSObject+Casting.h>
|
||||
#import <SessionUIKit/SessionUIKit.h>
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
#import "AboutTableViewController.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSPreferences.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SessionMessagingKit/OWSPreferences.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage.h>
|
||||
#import <SignalUtilitiesKit/TSDatabaseView.h>
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
#import <SessionMessagingKit/TSDatabaseView.h>
|
||||
|
||||
@implementation AboutTableViewController
|
||||
|
||||
|
|
|
@ -19,10 +19,6 @@ public class AccountManager: NSObject {
|
|||
return OWSProfileManager.shared()
|
||||
}
|
||||
|
||||
private var networkManager: TSNetworkManager {
|
||||
return SSKEnvironment.shared.networkManager
|
||||
}
|
||||
|
||||
private var preferences: OWSPreferences {
|
||||
return Environment.shared.preferences
|
||||
}
|
||||
|
@ -119,28 +115,4 @@ public class AccountManager: NSObject {
|
|||
let anyPromise = tsAccountManager.setIsManualMessageFetchEnabled(true)
|
||||
return Promise(anyPromise).asVoid()
|
||||
}
|
||||
|
||||
// MARK: Turn Server
|
||||
|
||||
func getTurnServerInfo() -> Promise<TurnServerInfo> {
|
||||
return Promise { resolver in
|
||||
self.networkManager.makeRequest(OWSRequestFactory.turnServerInfoRequest(),
|
||||
success: { (_: URLSessionDataTask, responseObject: Any?) in
|
||||
guard responseObject != nil else {
|
||||
return resolver.reject(OWSErrorMakeUnableToProcessServerResponseError())
|
||||
}
|
||||
|
||||
if let responseDictionary = responseObject as? [String: AnyObject] {
|
||||
if let turnServerInfo = TurnServerInfo(attributes: responseDictionary) {
|
||||
return resolver.fulfill(turnServerInfo)
|
||||
}
|
||||
Logger.error("unexpected server response:\(responseDictionary)")
|
||||
}
|
||||
return resolver.reject(OWSErrorMakeUnableToProcessServerResponseError())
|
||||
},
|
||||
failure: { (_: URLSessionDataTask, error: Error) in
|
||||
return resolver.reject(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,145 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import ContactsUI
|
||||
|
||||
class AddContactShareToExistingContactViewController: ContactsPicker, ContactsPickerDelegate, CNContactViewControllerDelegate {
|
||||
|
||||
// TODO - there are some hard coded assumptions in this VC that assume we are *pushed* onto a
|
||||
// navigation controller. That seems fine for now, but if we need to be presented as a modal,
|
||||
// or need to notify our presenter about our dismisall or other contact actions, a delegate
|
||||
// would be helpful. It seems like this would require some broad changes to the ContactShareViewHelper,
|
||||
// so I've left it as is for now, since it happens to work.
|
||||
// weak var addToExistingContactDelegate: AddContactShareToExistingContactViewControllerDelegate?
|
||||
|
||||
let contactShare: ContactShareViewModel
|
||||
|
||||
required init(contactShare: ContactShareViewModel) {
|
||||
self.contactShare = contactShare
|
||||
super.init(allowsMultipleSelection: false, subtitleCellType: .none)
|
||||
|
||||
self.contactsPickerDelegate = self
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
@objc required public init(allowsMultipleSelection: Bool, subtitleCellType: SubtitleCellValue) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
// MARK: - ContactsPickerDelegate
|
||||
|
||||
func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError) {
|
||||
owsFailDebug("with error: \(error)")
|
||||
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
navigationController.popViewController(animated: true)
|
||||
}
|
||||
|
||||
func contactsPickerDidCancel(_: ContactsPicker) {
|
||||
Logger.debug("")
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
navigationController.popViewController(animated: true)
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, didSelectContact oldContact: Contact) {
|
||||
Logger.debug("")
|
||||
|
||||
let contactsManager = Environment.shared.contactsManager
|
||||
guard let oldCNContact = contactsManager?.cnContact(withId: oldContact.cnContactId) else {
|
||||
owsFailDebug("could not load old CNContact.")
|
||||
return
|
||||
}
|
||||
guard let newCNContact = OWSContacts.systemContact(for: self.contactShare.dbRecord, imageData: self.contactShare.avatarImageData) else {
|
||||
owsFailDebug("could not load new CNContact.")
|
||||
return
|
||||
}
|
||||
merge(oldCNContact: oldCNContact, newCNContact: newCNContact)
|
||||
}
|
||||
|
||||
func merge(oldCNContact: CNContact, newCNContact: CNContact) {
|
||||
Logger.debug("")
|
||||
|
||||
let mergedCNContact: CNContact = Contact.merge(cnContact: oldCNContact, newCNContact: newCNContact)
|
||||
|
||||
// Not actually a "new" contact, but this brings up the edit form rather than the "Read" form
|
||||
// saving our users a tap in some cases when we already know they want to edit.
|
||||
let contactViewController: CNContactViewController = CNContactViewController(forNewContact: mergedCNContact)
|
||||
|
||||
// Default title is "New Contact". We could give a more descriptive title, but anything
|
||||
// seems redundant - the context is sufficiently clear.
|
||||
contactViewController.title = ""
|
||||
contactViewController.allowsActions = false
|
||||
contactViewController.allowsEditing = true
|
||||
contactViewController.delegate = self
|
||||
|
||||
let modal = OWSNavigationController(rootViewController: contactViewController)
|
||||
self.present(modal, animated: true)
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact]) {
|
||||
Logger.debug("")
|
||||
owsFailDebug("only supports single contact select")
|
||||
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
navigationController.popViewController(animated: true)
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - CNContactViewControllerDelegate
|
||||
|
||||
public func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
|
||||
Logger.debug("")
|
||||
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO this is weird - ideally we'd do something like
|
||||
// self.delegate?.didFinishAddingContact
|
||||
// and the delegate, which knows about our presentation context could do the right thing.
|
||||
//
|
||||
// As it is, we happen to always be *pushing* this view controller onto a navcontroller, so the
|
||||
// following works in all current cases.
|
||||
//
|
||||
// If we ever wanted to do something different, like present this in a modal, we'd have to rethink.
|
||||
|
||||
// We want to pop *this* view *and* the still presented CNContactViewController in a single animation.
|
||||
// Note this happens for *cancel* and for *done*. Unfortunately, I don't know of a way to detect the difference
|
||||
// between the two, since both just call this method.
|
||||
guard let myIndex = navigationController.viewControllers.firstIndex(of: self) else {
|
||||
owsFailDebug("myIndex was unexpectedly nil")
|
||||
navigationController.popViewController(animated: true)
|
||||
navigationController.popViewController(animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
let previousViewControllerIndex = navigationController.viewControllers.index(before: myIndex)
|
||||
let previousViewController = navigationController.viewControllers[previousViewControllerIndex]
|
||||
|
||||
self.dismiss(animated: false) {
|
||||
navigationController.popToViewController(previousViewController, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
#import "AddToBlockListViewController.h"
|
||||
#import "BlockListUIUtils.h"
|
||||
#import "ContactsViewHelper.h"
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SessionMessagingKit/SSKEnvironment.h>
|
||||
#import <SessionMessagingKit/OWSBlockingManager.h>
|
||||
#import <SignalUtilitiesKit/SignalAccount.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
@ -51,8 +51,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
__weak AddToBlockListViewController *weakSelf = self;
|
||||
[BlockListUIUtils showBlockPhoneNumberActionSheet:phoneNumber
|
||||
fromViewController:self
|
||||
blockingManager:self.contactsViewHelper.blockingManager
|
||||
contactsManager:self.contactsViewHelper.contactsManager
|
||||
blockingManager:SSKEnvironment.shared.blockingManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
if (isBlocked) {
|
||||
[weakSelf.navigationController popViewControllerAnimated:YES];
|
||||
|
@ -65,7 +64,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSAssertDebug(signalAccount);
|
||||
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
return ![helper isRecipientIdBlocked:signalAccount.recipientId];
|
||||
return ![SSKEnvironment.shared.blockingManager isRecipientIdBlocked:signalAccount.recipientId];
|
||||
}
|
||||
|
||||
- (void)signalAccountWasSelected:(SignalAccount *)signalAccount
|
||||
|
@ -73,15 +72,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSAssertDebug(signalAccount);
|
||||
|
||||
__weak AddToBlockListViewController *weakSelf = self;
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
if ([helper isRecipientIdBlocked:signalAccount.recipientId]) {
|
||||
if ([SSKEnvironment.shared.blockingManager isRecipientIdBlocked:signalAccount.recipientId]) {
|
||||
OWSFailDebug(@"Cannot add already blocked user to block list.");
|
||||
return;
|
||||
}
|
||||
[BlockListUIUtils showBlockSignalAccountActionSheet:signalAccount
|
||||
fromViewController:self
|
||||
blockingManager:helper.blockingManager
|
||||
contactsManager:helper.contactsManager
|
||||
blockingManager:SSKEnvironment.shared.blockingManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
if (isBlocked) {
|
||||
[weakSelf.navigationController popViewControllerAnimated:YES];
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SelectRecipientViewController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol AddToGroupViewControllerDelegate <NSObject>
|
||||
|
||||
- (void)recipientIdWasAdded:(NSString *)recipientId;
|
||||
|
||||
- (BOOL)isRecipientGroupMember:(NSString *)recipientId;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface AddToGroupViewController : SelectRecipientViewController
|
||||
|
||||
@property (nonatomic, weak) id<AddToGroupViewControllerDelegate> addToGroupDelegate;
|
||||
|
||||
@property (nonatomic) BOOL hideContacts;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,174 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AddToGroupViewController.h"
|
||||
#import "BlockListUIUtils.h"
|
||||
#import "ContactsViewHelper.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/SignalAccount.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AddToGroupViewController () <SelectRecipientViewControllerDelegate>
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation AddToGroupViewController
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
self.delegate = self;
|
||||
|
||||
[super loadView];
|
||||
|
||||
self.title = NSLocalizedString(@"ADD_GROUP_MEMBER_VIEW_TITLE", @"Title for the 'add group member' view.");
|
||||
}
|
||||
|
||||
- (NSString *)phoneNumberSectionTitle
|
||||
{
|
||||
return NSLocalizedString(@"ADD_GROUP_MEMBER_VIEW_PHONE_NUMBER_TITLE",
|
||||
@"Title for the 'add by phone number' section of the 'add group member' view.");
|
||||
}
|
||||
|
||||
- (NSString *)phoneNumberButtonText
|
||||
{
|
||||
return NSLocalizedString(@"ADD_GROUP_MEMBER_VIEW_BUTTON",
|
||||
@"A label for the 'add by phone number' button in the 'add group member' view");
|
||||
}
|
||||
|
||||
- (NSString *)contactsSectionTitle
|
||||
{
|
||||
return NSLocalizedString(
|
||||
@"ADD_GROUP_MEMBER_VIEW_CONTACT_TITLE", @"Title for the 'add contact' section of the 'add group member' view.");
|
||||
}
|
||||
|
||||
- (void)phoneNumberWasSelected:(NSString *)phoneNumber
|
||||
{
|
||||
OWSAssertDebug(phoneNumber.length > 0);
|
||||
|
||||
__weak AddToGroupViewController *weakSelf = self;
|
||||
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
if ([helper isRecipientIdBlocked:phoneNumber]) {
|
||||
[BlockListUIUtils showUnblockPhoneNumberActionSheet:phoneNumber
|
||||
fromViewController:self
|
||||
blockingManager:helper.blockingManager
|
||||
contactsManager:helper.contactsManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
if (!isBlocked) {
|
||||
[weakSelf addToGroup:phoneNumber];
|
||||
}
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL didShowSNAlert = [SafetyNumberConfirmationAlert
|
||||
presentAlertIfNecessaryWithRecipientId:phoneNumber
|
||||
confirmationText:
|
||||
NSLocalizedString(@"SAFETY_NUMBER_CHANGED_CONFIRM_ADD_TO_GROUP_ACTION",
|
||||
@"button title to confirm adding a recipient to a group when their safety "
|
||||
@"number has recently changed")
|
||||
contactsManager:helper.contactsManager
|
||||
completion:^(BOOL didConfirmIdentity) {
|
||||
if (didConfirmIdentity) {
|
||||
[weakSelf addToGroup:phoneNumber];
|
||||
}
|
||||
}];
|
||||
if (didShowSNAlert) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self addToGroup:phoneNumber];
|
||||
}
|
||||
|
||||
- (BOOL)canSignalAccountBeSelected:(SignalAccount *)signalAccount
|
||||
{
|
||||
OWSAssertDebug(signalAccount);
|
||||
|
||||
return ![self.addToGroupDelegate isRecipientGroupMember:signalAccount.recipientId];
|
||||
}
|
||||
|
||||
- (void)signalAccountWasSelected:(SignalAccount *)signalAccount
|
||||
{
|
||||
OWSAssertDebug(signalAccount);
|
||||
|
||||
__weak AddToGroupViewController *weakSelf = self;
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
if ([self.addToGroupDelegate isRecipientGroupMember:signalAccount.recipientId]) {
|
||||
OWSFailDebug(@"Cannot add user to group member if already a member.");
|
||||
return;
|
||||
}
|
||||
|
||||
if ([helper isRecipientIdBlocked:signalAccount.recipientId]) {
|
||||
[BlockListUIUtils showUnblockSignalAccountActionSheet:signalAccount
|
||||
fromViewController:self
|
||||
blockingManager:helper.blockingManager
|
||||
contactsManager:helper.contactsManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
if (!isBlocked) {
|
||||
[weakSelf addToGroup:signalAccount.recipientId];
|
||||
}
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL didShowSNAlert = [SafetyNumberConfirmationAlert
|
||||
presentAlertIfNecessaryWithRecipientId:signalAccount.recipientId
|
||||
confirmationText:
|
||||
NSLocalizedString(@"SAFETY_NUMBER_CHANGED_CONFIRM_ADD_TO_GROUP_ACTION",
|
||||
@"button title to confirm adding a recipient to a group when their safety "
|
||||
@"number has recently changed")
|
||||
contactsManager:helper.contactsManager
|
||||
completion:^(BOOL didConfirmIdentity) {
|
||||
if (didConfirmIdentity) {
|
||||
[weakSelf addToGroup:signalAccount.recipientId];
|
||||
}
|
||||
}];
|
||||
if (didShowSNAlert) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self addToGroup:signalAccount.recipientId];
|
||||
}
|
||||
|
||||
- (void)addToGroup:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
[self.addToGroupDelegate recipientIdWasAdded:recipientId];
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
- (BOOL)shouldHideLocalNumber
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)shouldHideContacts
|
||||
{
|
||||
return self.hideContacts;
|
||||
}
|
||||
|
||||
- (BOOL)shouldValidatePhoneNumbers
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (nullable NSString *)accessoryMessageForSignalAccount:(SignalAccount *)signalAccount
|
||||
{
|
||||
OWSAssertDebug(signalAccount);
|
||||
|
||||
if ([self.addToGroupDelegate isRecipientGroupMember:signalAccount.recipientId]) {
|
||||
return NSLocalizedString(@"NEW_GROUP_MEMBER_LABEL", @"An indicator that a user is a member of the new group.");
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,9 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSTableViewController.h"
|
||||
|
||||
@interface AdvancedSettingsTableViewController : OWSTableViewController
|
||||
|
||||
@end
|
|
@ -1,312 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AdvancedSettingsTableViewController.h"
|
||||
#import "DebugLogger.h"
|
||||
#import "DomainFrontingCountryViewController.h"
|
||||
#import "OWSCountryMetadata.h"
|
||||
#import "Pastelog.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "TSAccountManager.h"
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <Reachability/Reachability.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSPreferences.h>
|
||||
#import <SignalUtilitiesKit/OWSSignalService.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AdvancedSettingsTableViewController ()
|
||||
|
||||
@property (nonatomic) Reachability *reachability;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation AdvancedSettingsTableViewController
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
self.title = NSLocalizedString(@"SETTINGS_ADVANCED_TITLE", @"");
|
||||
|
||||
self.reachability = [Reachability reachabilityForInternetConnection];
|
||||
|
||||
[self observeNotifications];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)observeNotifications
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(socketStateDidChange)
|
||||
name:kNSNotification_OWSWebSocketStateDidChange
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(reachabilityChanged)
|
||||
name:kReachabilityChangedNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)socketStateDidChange
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)reachabilityChanged
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - Table Contents
|
||||
|
||||
- (void)updateTableContents
|
||||
{
|
||||
OWSTableContents *contents = [OWSTableContents new];
|
||||
|
||||
__weak AdvancedSettingsTableViewController *weakSelf = self;
|
||||
|
||||
OWSTableSection *loggingSection = [OWSTableSection new];
|
||||
loggingSection.headerTitle = NSLocalizedString(@"LOGGING_SECTION", nil);
|
||||
[loggingSection addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"SETTINGS_ADVANCED_DEBUGLOG", @"")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"enable_debug_log")
|
||||
isOnBlock:^{
|
||||
return [OWSPreferences isLoggingEnabled];
|
||||
}
|
||||
isEnabledBlock:^{
|
||||
return YES;
|
||||
}
|
||||
target:weakSelf
|
||||
selector:@selector(didToggleEnableLogSwitch:)]];
|
||||
|
||||
|
||||
if ([OWSPreferences isLoggingEnabled]) {
|
||||
[loggingSection
|
||||
addItem:[OWSTableItem actionItemWithText:NSLocalizedString(@"SETTINGS_ADVANCED_SUBMIT_DEBUGLOG", @"")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"submit_debug_log")
|
||||
actionBlock:^{
|
||||
OWSLogInfo(@"Submitting debug logs");
|
||||
[DDLog flushLog];
|
||||
[Pastelog submitLogs];
|
||||
}]];
|
||||
}
|
||||
|
||||
[contents addSection:loggingSection];
|
||||
|
||||
OWSTableSection *pushNotificationsSection = [OWSTableSection new];
|
||||
pushNotificationsSection.headerTitle
|
||||
= NSLocalizedString(@"PUSH_REGISTER_TITLE", @"Used in table section header and alert view title contexts");
|
||||
[pushNotificationsSection addItem:[OWSTableItem actionItemWithText:NSLocalizedString(@"REREGISTER_FOR_PUSH", nil)
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(
|
||||
self, @"reregister_push_notifications")
|
||||
actionBlock:^{
|
||||
[weakSelf syncPushTokens];
|
||||
}]];
|
||||
[contents addSection:pushNotificationsSection];
|
||||
|
||||
// Censorship circumvention has certain disadvantages so it should only be
|
||||
// used if necessary. Therefore:
|
||||
//
|
||||
// * We disable this setting if the user has a phone number from a censored region -
|
||||
// censorship circumvention will be auto-activated for this user.
|
||||
// * We disable this setting if the user is already connected; they're not being
|
||||
// censored.
|
||||
// * We continue to show this setting so long as it is set to allow users to disable
|
||||
// it, for example when they leave a censored region.
|
||||
OWSTableSection *censorshipSection = [OWSTableSection new];
|
||||
censorshipSection.headerTitle = NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_HEADER",
|
||||
@"Table header for the 'censorship circumvention' section.");
|
||||
BOOL isAnySocketOpen = TSSocketManager.shared.highestSocketState == OWSWebSocketStateOpen;
|
||||
if (OWSSignalService.sharedInstance.hasCensoredPhoneNumber) {
|
||||
if (OWSSignalService.sharedInstance.isCensorshipCircumventionManuallyDisabled) {
|
||||
censorshipSection.footerTitle
|
||||
= NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_FOOTER_MANUALLY_DISABLED",
|
||||
@"Table footer for the 'censorship circumvention' section shown when censorship circumvention has "
|
||||
@"been manually disabled.");
|
||||
} else {
|
||||
censorshipSection.footerTitle = NSLocalizedString(
|
||||
@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_FOOTER_AUTO_ENABLED",
|
||||
@"Table footer for the 'censorship circumvention' section shown when censorship circumvention has been "
|
||||
@"auto-enabled based on local phone number.");
|
||||
}
|
||||
} else if (isAnySocketOpen) {
|
||||
censorshipSection.footerTitle
|
||||
= NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_FOOTER_WEBSOCKET_CONNECTED",
|
||||
@"Table footer for the 'censorship circumvention' section shown when the app is connected to the "
|
||||
@"Signal service.");
|
||||
} else if (!self.reachability.isReachable) {
|
||||
censorshipSection.footerTitle
|
||||
= NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_FOOTER_NO_CONNECTION",
|
||||
@"Table footer for the 'censorship circumvention' section shown when the app is not connected to the "
|
||||
@"internet.");
|
||||
} else {
|
||||
censorshipSection.footerTitle = NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_FOOTER",
|
||||
@"Table footer for the 'censorship circumvention' section when censorship circumvention can be manually "
|
||||
@"enabled.");
|
||||
}
|
||||
|
||||
// Do enable if :
|
||||
//
|
||||
// * ...Censorship circumvention is already manually enabled (to allow users to disable it).
|
||||
//
|
||||
// Otherwise, don't enable if:
|
||||
//
|
||||
// * ...Censorship circumvention is already enabled based on the local phone number.
|
||||
// * ...The websocket is connected, since that demonstrates that no censorship is in effect.
|
||||
// * ...The internet is not reachable, since we don't want to let users to activate
|
||||
// censorship circumvention unnecessarily, e.g. if they just don't have a valid
|
||||
// internet connection.
|
||||
OWSTableSwitchBlock isCensorshipCircumventionOnBlock = ^{
|
||||
return OWSSignalService.sharedInstance.isCensorshipCircumventionActive;
|
||||
};
|
||||
Reachability *reachability = self.reachability;
|
||||
OWSTableSwitchBlock isManualCensorshipCircumventionOnEnabledBlock = ^{
|
||||
OWSSignalService *service = OWSSignalService.sharedInstance;
|
||||
if (service.isCensorshipCircumventionActive) {
|
||||
return YES;
|
||||
} else if (service.hasCensoredPhoneNumber && service.isCensorshipCircumventionManuallyDisabled) {
|
||||
return YES;
|
||||
} else if (TSSocketManager.shared.highestSocketState == OWSWebSocketStateOpen) {
|
||||
return NO;
|
||||
} else {
|
||||
return reachability.isReachable;
|
||||
}
|
||||
};
|
||||
|
||||
[censorshipSection
|
||||
addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION",
|
||||
@"Label for the 'manual censorship circumvention' switch.")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"censorship_circumvention")
|
||||
isOnBlock:isCensorshipCircumventionOnBlock
|
||||
isEnabledBlock:isManualCensorshipCircumventionOnEnabledBlock
|
||||
target:weakSelf
|
||||
selector:@selector(didToggleEnableCensorshipCircumventionSwitch:)]];
|
||||
|
||||
if (OWSSignalService.sharedInstance.isCensorshipCircumventionManuallyActivated) {
|
||||
OWSCountryMetadata *manualCensorshipCircumventionCountry =
|
||||
[weakSelf ensureManualCensorshipCircumventionCountry];
|
||||
OWSAssertDebug(manualCensorshipCircumventionCountry);
|
||||
NSString *text = [NSString
|
||||
stringWithFormat:NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_COUNTRY_FORMAT",
|
||||
@"Label for the 'manual censorship circumvention' country. Embeds {{the manual "
|
||||
@"censorship circumvention country}}."),
|
||||
manualCensorshipCircumventionCountry.localizedCountryName];
|
||||
[censorshipSection addItem:[OWSTableItem disclosureItemWithText:text
|
||||
actionBlock:^{
|
||||
[weakSelf showDomainFrontingCountryView];
|
||||
}]];
|
||||
}
|
||||
[contents addSection:censorshipSection];
|
||||
|
||||
self.contents = contents;
|
||||
}
|
||||
|
||||
- (void)showDomainFrontingCountryView
|
||||
{
|
||||
DomainFrontingCountryViewController *vc = [DomainFrontingCountryViewController new];
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
|
||||
- (OWSCountryMetadata *)ensureManualCensorshipCircumventionCountry
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSCountryMetadata *countryMetadata = nil;
|
||||
NSString *countryCode = OWSSignalService.sharedInstance.manualCensorshipCircumventionCountryCode;
|
||||
if (countryCode) {
|
||||
countryMetadata = [OWSCountryMetadata countryMetadataForCountryCode:countryCode];
|
||||
}
|
||||
|
||||
if (!countryMetadata) {
|
||||
countryCode = [PhoneNumber defaultCountryCode];
|
||||
if (countryCode) {
|
||||
countryMetadata = [OWSCountryMetadata countryMetadataForCountryCode:countryCode];
|
||||
}
|
||||
}
|
||||
|
||||
if (!countryMetadata) {
|
||||
countryCode = @"US";
|
||||
countryMetadata = [OWSCountryMetadata countryMetadataForCountryCode:countryCode];
|
||||
OWSAssertDebug(countryMetadata);
|
||||
}
|
||||
|
||||
if (countryMetadata) {
|
||||
// Ensure the "manual censorship circumvention" country state is in sync.
|
||||
OWSSignalService.sharedInstance.manualCensorshipCircumventionCountryCode = countryCode;
|
||||
}
|
||||
|
||||
return countryMetadata;
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)syncPushTokens
|
||||
{
|
||||
OWSSyncPushTokensJob *job =
|
||||
[[OWSSyncPushTokensJob alloc] initWithAccountManager:AppEnvironment.shared.accountManager
|
||||
preferences:Environment.shared.preferences];
|
||||
job.uploadOnlyIfStale = NO;
|
||||
[job run]
|
||||
.then(^{
|
||||
[OWSAlerts showAlertWithTitle:NSLocalizedString(@"PUSH_REGISTER_SUCCESS",
|
||||
@"Title of alert shown when push tokens sync job succeeds.")];
|
||||
})
|
||||
.catch(^(NSError *error) {
|
||||
[OWSAlerts showAlertWithTitle:NSLocalizedString(@"REGISTRATION_BODY",
|
||||
@"Title of alert shown when push tokens sync job fails.")];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)didToggleEnableLogSwitch:(UISwitch *)sender
|
||||
{
|
||||
if (!sender.isOn) {
|
||||
OWSLogInfo(@"disabling logging.");
|
||||
[[DebugLogger sharedLogger] wipeLogs];
|
||||
[[DebugLogger sharedLogger] disableFileLogging];
|
||||
} else {
|
||||
[[DebugLogger sharedLogger] enableFileLogging];
|
||||
OWSLogInfo(@"enabling logging.");
|
||||
}
|
||||
|
||||
[OWSPreferences setIsLoggingEnabled:sender.isOn];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)didToggleEnableCensorshipCircumventionSwitch:(UISwitch *)sender
|
||||
{
|
||||
OWSSignalService *service = OWSSignalService.sharedInstance;
|
||||
if (sender.isOn) {
|
||||
service.isCensorshipCircumventionManuallyDisabled = NO;
|
||||
service.isCensorshipCircumventionManuallyActivated = YES;
|
||||
} else {
|
||||
service.isCensorshipCircumventionManuallyDisabled = YES;
|
||||
service.isCensorshipCircumventionManuallyActivated = NO;
|
||||
}
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -8,39 +8,28 @@
|
|||
#import "OWSBackup.h"
|
||||
#import "OWSOrphanDataCleaner.h"
|
||||
#import "OWSScreenLockUI.h"
|
||||
#import "Pastelog.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "SignalApp.h"
|
||||
#import "SignalsNavigationController.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalCoreKit/iOSVersions.h>
|
||||
#import <SignalUtilitiesKit/AppSetup.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSNavigationController.h>
|
||||
#import <SignalUtilitiesKit/OWSPreferences.h>
|
||||
#import <SessionMessagingKit/OWSPreferences.h>
|
||||
#import <SignalUtilitiesKit/OWSProfileManager.h>
|
||||
#import <SignalUtilitiesKit/VersionMigrations.h>
|
||||
#import <SignalUtilitiesKit/AppReadiness.h>
|
||||
#import <SignalUtilitiesKit/NSUserDefaults+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWS2FAManager.h>
|
||||
#import <SignalUtilitiesKit/OWSBatchMessageProcessor.h>
|
||||
#import <SignalUtilitiesKit/OWSDisappearingMessagesJob.h>
|
||||
#import <SessionMessagingKit/AppReadiness.h>
|
||||
#import <SessionUtilitiesKit/NSUserDefaults+OWS.h>
|
||||
#import <SessionMessagingKit/OWSDisappearingMessagesJob.h>
|
||||
#import <SignalUtilitiesKit/OWSFailedAttachmentDownloadsJob.h>
|
||||
#import <SignalUtilitiesKit/OWSFailedMessagesJob.h>
|
||||
#import <SignalUtilitiesKit/OWSIncompleteCallsJob.h>
|
||||
#import <SignalUtilitiesKit/OWSMath.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageManager.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageSender.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+Calling.h>
|
||||
#import <SignalUtilitiesKit/OWSReadReceiptManager.h>
|
||||
#import <SignalUtilitiesKit/SSKEnvironment.h>
|
||||
#import <SessionUtilitiesKit/OWSMath.h>
|
||||
#import <SessionMessagingKit/OWSReadReceiptManager.h>
|
||||
#import <SessionMessagingKit/SSKEnvironment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/TSAccountManager.h>
|
||||
#import <SignalUtilitiesKit/TSDatabaseView.h>
|
||||
#import <SessionMessagingKit/TSAccountManager.h>
|
||||
#import <SessionMessagingKit/TSDatabaseView.h>
|
||||
#import <SignalUtilitiesKit/TSPreKeyManager.h>
|
||||
#import <SignalUtilitiesKit/TSSocketManager.h>
|
||||
#import <YapDatabase/YapDatabaseCryptoUtils.h>
|
||||
#import <sys/utsname.h>
|
||||
|
||||
|
@ -118,20 +107,6 @@ static NSTimeInterval launchStartedAt;
|
|||
return SSKEnvironment.shared.disappearingMessagesJob;
|
||||
}
|
||||
|
||||
- (TSSocketManager *)socketManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.socketManager);
|
||||
|
||||
return SSKEnvironment.shared.socketManager;
|
||||
}
|
||||
|
||||
- (OWSMessageManager *)messageManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.messageManager);
|
||||
|
||||
return SSKEnvironment.shared.messageManager;
|
||||
}
|
||||
|
||||
- (OWSWindowManager *)windowManager
|
||||
{
|
||||
return Environment.shared.windowManager;
|
||||
|
@ -220,8 +195,6 @@ static NSTimeInterval launchStartedAt;
|
|||
|
||||
[AppVersion sharedInstance];
|
||||
|
||||
[self startupLogging];
|
||||
|
||||
// Prevent the device from sleeping during database view async registration
|
||||
// (e.g. long database upgrades).
|
||||
//
|
||||
|
@ -279,18 +252,12 @@ static NSTimeInterval launchStartedAt;
|
|||
selector:@selector(registrationStateDidChange)
|
||||
name:RegistrationStateDidChangeNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(registrationLockDidChange:)
|
||||
name:NSNotificationName_2FAStateDidChange
|
||||
object:nil];
|
||||
|
||||
// Loki - Observe data nuke request notifications
|
||||
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(handleDataNukeRequested:) name:NSNotification.dataNukeRequested object:nil];
|
||||
|
||||
OWSLogInfo(@"application: didFinishLaunchingWithOptions completed.");
|
||||
|
||||
[OWSAnalytics appLaunchDidBegin];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
@ -394,75 +361,6 @@ static NSTimeInterval launchStartedAt;
|
|||
}
|
||||
}
|
||||
|
||||
- (void)showLaunchFailureUI:(NSError *)error
|
||||
{
|
||||
// Disable normal functioning of app.
|
||||
self.didAppLaunchFail = YES;
|
||||
|
||||
// We perform a subset of the [application:didFinishLaunchingWithOptions:].
|
||||
[AppVersion sharedInstance];
|
||||
[self startupLogging];
|
||||
|
||||
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
|
||||
// Show the launch screen
|
||||
self.window.rootViewController =
|
||||
[[UIStoryboard storyboardWithName:@"Launch Screen" bundle:nil] instantiateInitialViewController];
|
||||
|
||||
[self.window makeKeyAndVisible];
|
||||
|
||||
UIAlertController *alert =
|
||||
[UIAlertController alertControllerWithTitle:NSLocalizedString(@"APP_LAUNCH_FAILURE_ALERT_TITLE",
|
||||
@"Title for the 'app launch failed' alert.")
|
||||
message:NSLocalizedString(@"APP_LAUNCH_FAILURE_ALERT_MESSAGE",
|
||||
@"Message for the 'app launch failed' alert.")
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"SETTINGS_ADVANCED_SUBMIT_DEBUGLOG", nil)
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[Pastelog submitLogsWithCompletion:^{
|
||||
OWSFail(@"Exiting after sharing debug logs.");
|
||||
}];
|
||||
}]];
|
||||
UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController];
|
||||
[fromViewController presentAlert:alert];
|
||||
}
|
||||
|
||||
- (void)startupLogging
|
||||
{
|
||||
OWSLogInfo(@"iOS Version: %@", [UIDevice currentDevice].systemVersion);
|
||||
|
||||
NSString *localeIdentifier = [NSLocale.currentLocale objectForKey:NSLocaleIdentifier];
|
||||
if (localeIdentifier.length > 0) {
|
||||
OWSLogInfo(@"Locale Identifier: %@", localeIdentifier);
|
||||
}
|
||||
NSString *countryCode = [NSLocale.currentLocale objectForKey:NSLocaleCountryCode];
|
||||
if (countryCode.length > 0) {
|
||||
OWSLogInfo(@"Country Code: %@", countryCode);
|
||||
}
|
||||
NSString *languageCode = [NSLocale.currentLocale objectForKey:NSLocaleLanguageCode];
|
||||
if (languageCode.length > 0) {
|
||||
OWSLogInfo(@"Language Code: %@", languageCode);
|
||||
}
|
||||
|
||||
struct utsname systemInfo;
|
||||
uname(&systemInfo);
|
||||
|
||||
OWSLogInfo(@"Device Model: %@ (%@)",
|
||||
UIDevice.currentDevice.model,
|
||||
[NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]);
|
||||
|
||||
NSDictionary<NSString *, NSString *> *buildDetails =
|
||||
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"BuildDetails"];
|
||||
OWSLogInfo(@"WebRTC Commit: %@", buildDetails[@"WebRTCCommit"]);
|
||||
OWSLogInfo(@"Build XCode Version: %@", buildDetails[@"XCodeVersion"]);
|
||||
OWSLogInfo(@"Build OS X Version: %@", buildDetails[@"OSXVersion"]);
|
||||
OWSLogInfo(@"Build Cocoapods Version: %@", buildDetails[@"CocoapodsVersion"]);
|
||||
OWSLogInfo(@"Build Carthage Version: %@", buildDetails[@"CarthageVersion"]);
|
||||
OWSLogInfo(@"Build Date/Time: %@", buildDetails[@"DateTime"]);
|
||||
}
|
||||
|
||||
- (void)enableBackgroundRefreshIfNecessary
|
||||
{
|
||||
[AppReadiness runNowOrWhenAppDidBecomeReady:^{
|
||||
|
@ -497,17 +395,11 @@ static NSTimeInterval launchStartedAt;
|
|||
// sent before the app exited should be marked as failures.
|
||||
[[[OWSFailedMessagesJob alloc] initWithPrimaryStorage:self.primaryStorage] run];
|
||||
[[[OWSFailedAttachmentDownloadsJob alloc] initWithPrimaryStorage:self.primaryStorage] run];
|
||||
|
||||
if (CurrentAppContext().isMainApp) {
|
||||
[SNJobQueue.shared resumePendingJobs];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
OWSLogInfo(@"Running post launch block for unregistered user.");
|
||||
|
||||
// Unregistered user should have no unread messages. e.g. if you delete your account.
|
||||
[AppEnvironment.shared.notificationPresenter clearAllNotifications];
|
||||
|
||||
UITapGestureRecognizer *gesture =
|
||||
[[UITapGestureRecognizer alloc] initWithTarget:[Pastelog class] action:@selector(submitLogs)];
|
||||
gesture.numberOfTapsRequired = 8;
|
||||
[self.window addGestureRecognizer:gesture];
|
||||
}
|
||||
}); // end dispatchOnce for first time we become active
|
||||
|
||||
|
@ -630,8 +522,7 @@ static NSTimeInterval launchStartedAt;
|
|||
// TODO: or something like that in production.
|
||||
[OWSOrphanDataCleaner auditOnLaunchIfNecessary];
|
||||
#endif
|
||||
|
||||
[self.profileManager fetchLocalUsersProfile];
|
||||
|
||||
[self.readReceiptManager prepareCachedValues];
|
||||
|
||||
// Disable the SAE until the main app has successfully completed launch process
|
||||
|
@ -640,8 +531,6 @@ static NSTimeInterval launchStartedAt;
|
|||
|
||||
[self ensureRootViewController];
|
||||
|
||||
[self.messageManager startObserving];
|
||||
|
||||
[self.udManager setup];
|
||||
|
||||
[self preheatDatabaseViews];
|
||||
|
@ -657,8 +546,6 @@ static NSTimeInterval launchStartedAt;
|
|||
if (appVersion.lastAppVersion.length > 0
|
||||
&& ![appVersion.lastAppVersion isEqualToString:appVersion.currentAppVersion]) {
|
||||
[[self.tsAccountManager updateAccountAttributes] retainUntilComplete];
|
||||
|
||||
[SSKEnvironment.shared.syncManager sendConfigurationSyncMessage];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -734,18 +621,6 @@ static NSTimeInterval launchStartedAt;
|
|||
[UIViewController attemptRotationToDeviceOrientation];
|
||||
}
|
||||
|
||||
#pragma mark - Status Bar Interaction
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
|
||||
{
|
||||
[super touchesBegan:touches withEvent:event];
|
||||
CGPoint location = [[[event allTouches] anyObject] locationInView:[self window]];
|
||||
CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame;
|
||||
if (CGRectContainsPoint(statusBarFrame, location)) {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:TappedStatusBarNotification object:nil];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
|
||||
|
@ -762,9 +637,9 @@ static NSTimeInterval launchStartedAt;
|
|||
OWSLogInfo(@"Registering for push notifications with token: %@.", deviceToken);
|
||||
BOOL isUsingFullAPNs = [NSUserDefaults.standardUserDefaults boolForKey:@"isUsingFullAPNs"];
|
||||
if (isUsingFullAPNs) {
|
||||
__unused AnyPromise *promise = [LKPushNotificationManager registerWithToken:deviceToken hexEncodedPublicKey:self.tsAccountManager.localNumber isForcedUpdate:NO];
|
||||
__unused AnyPromise *promise = [LKPushNotificationAPI registerWithToken:deviceToken hexEncodedPublicKey:self.tsAccountManager.localNumber isForcedUpdate:NO];
|
||||
} else {
|
||||
__unused AnyPromise *promise = [LKPushNotificationManager unregisterWithToken:deviceToken isForcedUpdate:NO];
|
||||
__unused AnyPromise *promise = [LKPushNotificationAPI unregisterWithToken:deviceToken isForcedUpdate:NO];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -901,7 +776,6 @@ static NSTimeInterval launchStartedAt;
|
|||
- (void)startOpenGroupPollersIfNeeded
|
||||
{
|
||||
[LKPublicChatManager.shared startPollersIfNeeded];
|
||||
[SSKEnvironment.shared.attachmentDownloads continueDownloadIfPossible];
|
||||
}
|
||||
|
||||
- (void)stopOpenGroupPollers { [LKPublicChatManager.shared stopPollers]; }
|
||||
|
@ -950,10 +824,9 @@ static NSTimeInterval launchStartedAt;
|
|||
NSString *hexEncodedDeviceToken = [userDefaults stringForKey:@"deviceToken"];
|
||||
if (isUsingFullAPNs && hexEncodedDeviceToken != nil) {
|
||||
NSData *deviceToken = [NSData dataFromHexString:hexEncodedDeviceToken];
|
||||
[[LKPushNotificationManager unregisterWithToken:deviceToken isForcedUpdate:YES] retainUntilComplete];
|
||||
[[LKPushNotificationAPI unregisterWithToken:deviceToken isForcedUpdate:YES] retainUntilComplete];
|
||||
}
|
||||
[ThreadUtil deleteAllContent];
|
||||
[SSKEnvironment.shared.messageSenderJobQueue clearAllJobs];
|
||||
[SSKEnvironment.shared.identityManager clearIdentityKey];
|
||||
[SNSnodeAPI clearSnodePool];
|
||||
[self stopPoller];
|
||||
|
|
|
@ -25,18 +25,6 @@ import SignalUtilitiesKit
|
|||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public var callMessageHandler: WebRTCCallMessageHandler
|
||||
|
||||
// @objc
|
||||
// public var callService: CallService
|
||||
|
||||
// @objc
|
||||
// public var outboundCallInitiator: OutboundCallInitiator
|
||||
|
||||
@objc
|
||||
public var messageFetcherJob: MessageFetcherJob
|
||||
|
||||
@objc
|
||||
public var accountManager: AccountManager
|
||||
|
||||
|
@ -80,10 +68,6 @@ import SignalUtilitiesKit
|
|||
public var backupLazyRestore: BackupLazyRestore
|
||||
|
||||
private override init() {
|
||||
self.callMessageHandler = WebRTCCallMessageHandler()
|
||||
// self.callService = CallService()
|
||||
// self.outboundCallInitiator = OutboundCallInitiator()
|
||||
self.messageFetcherJob = MessageFetcherJob()
|
||||
self.accountManager = AccountManager()
|
||||
self.notificationPresenter = NotificationPresenter()
|
||||
self.pushRegistrationManager = PushRegistrationManager()
|
||||
|
@ -102,10 +86,7 @@ import SignalUtilitiesKit
|
|||
|
||||
@objc
|
||||
public func setup() {
|
||||
// callService.createCallUIAdapter()
|
||||
|
||||
// Hang certain singletons on SSKEnvironment too.
|
||||
SSKEnvironment.shared.notificationsManager = notificationPresenter
|
||||
// SSKEnvironment.shared.callMessageHandler = callMessageHandler
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,14 +120,6 @@ protocol NotificationPresenterAdaptee: class {
|
|||
|
||||
func cancelNotifications(threadId: String)
|
||||
func clearAllNotifications()
|
||||
|
||||
var hasReceivedSyncMessageRecently: Bool { get }
|
||||
}
|
||||
|
||||
extension NotificationPresenterAdaptee {
|
||||
var hasReceivedSyncMessageRecently: Bool {
|
||||
return OWSDeviceManager.shared().hasReceivedSyncMessage(inLastSeconds: 60)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(OWSNotificationPresenter)
|
||||
|
@ -153,10 +145,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
|
||||
// MARK: - Dependencies
|
||||
|
||||
var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
var identityManager: OWSIdentityManager {
|
||||
return OWSIdentityManager.shared()
|
||||
}
|
||||
|
@ -209,140 +197,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
return adaptee.registerNotificationSettings()
|
||||
}
|
||||
|
||||
// func presentIncomingCall(_ call: SignalCall, callerName: String) {
|
||||
//
|
||||
// let notificationTitle: String?
|
||||
// switch previewType {
|
||||
// case .noNameNoPreview:
|
||||
// notificationTitle = nil
|
||||
// case .nameNoPreview, .namePreview:
|
||||
// notificationTitle = callerName
|
||||
// }
|
||||
// let notificationBody = NotificationStrings.incomingCallBody
|
||||
//
|
||||
// let remotePhoneNumber = call.remotePhoneNumber
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber)
|
||||
//
|
||||
// guard let threadId = thread.uniqueId else {
|
||||
// owsFailDebug("threadId was unexpectedly nil")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let userInfo = [
|
||||
// AppNotificationUserInfoKey.threadId: threadId,
|
||||
// AppNotificationUserInfoKey.localCallId: call.localId.uuidString
|
||||
// ]
|
||||
//
|
||||
// DispatchQueue.main.async {
|
||||
// self.adaptee.notify(category: .incomingCall,
|
||||
// title: notificationTitle,
|
||||
// body: notificationBody,
|
||||
// userInfo: userInfo,
|
||||
// sound: .defaultiOSIncomingRingtone,
|
||||
// replacingIdentifier: call.localId.uuidString)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func presentMissedCall(_ call: SignalCall, callerName: String) {
|
||||
// let notificationTitle: String?
|
||||
// switch previewType {
|
||||
// case .noNameNoPreview:
|
||||
// notificationTitle = nil
|
||||
// case .nameNoPreview, .namePreview:
|
||||
// notificationTitle = callerName
|
||||
// }
|
||||
// let notificationBody = NotificationStrings.missedCallBody
|
||||
//
|
||||
// let remotePhoneNumber = call.remotePhoneNumber
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber)
|
||||
//
|
||||
// guard let threadId = thread.uniqueId else {
|
||||
// owsFailDebug("threadId was unexpectedly nil")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let userInfo = [
|
||||
// AppNotificationUserInfoKey.threadId: threadId,
|
||||
// AppNotificationUserInfoKey.callBackNumber: remotePhoneNumber
|
||||
// ]
|
||||
//
|
||||
// DispatchQueue.main.async {
|
||||
// let sound = self.requestSound(thread: thread)
|
||||
// self.adaptee.notify(category: .missedCall,
|
||||
// title: notificationTitle,
|
||||
// body: notificationBody,
|
||||
// userInfo: userInfo,
|
||||
// sound: sound,
|
||||
// replacingIdentifier: call.localId.uuidString)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public func presentMissedCallBecauseOfNoLongerVerifiedIdentity(call: SignalCall, callerName: String) {
|
||||
// let notificationTitle: String?
|
||||
// switch previewType {
|
||||
// case .noNameNoPreview:
|
||||
// notificationTitle = nil
|
||||
// case .nameNoPreview, .namePreview:
|
||||
// notificationTitle = callerName
|
||||
// }
|
||||
// let notificationBody = NotificationStrings.missedCallBecauseOfIdentityChangeBody
|
||||
//
|
||||
// let remotePhoneNumber = call.remotePhoneNumber
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber)
|
||||
// guard let threadId = thread.uniqueId else {
|
||||
// owsFailDebug("threadId was unexpectedly nil")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let userInfo = [
|
||||
// AppNotificationUserInfoKey.threadId: threadId
|
||||
// ]
|
||||
//
|
||||
// DispatchQueue.main.async {
|
||||
// let sound = self.requestSound(thread: thread)
|
||||
// self.adaptee.notify(category: .missedCallFromNoLongerVerifiedIdentity,
|
||||
// title: notificationTitle,
|
||||
// body: notificationBody,
|
||||
// userInfo: userInfo,
|
||||
// sound: sound,
|
||||
// replacingIdentifier: call.localId.uuidString)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public func presentMissedCallBecauseOfNewIdentity(call: SignalCall, callerName: String) {
|
||||
// let notificationTitle: String?
|
||||
// switch previewType {
|
||||
// case .noNameNoPreview:
|
||||
// notificationTitle = nil
|
||||
// case .nameNoPreview, .namePreview:
|
||||
// notificationTitle = callerName
|
||||
// }
|
||||
// let notificationBody = NotificationStrings.missedCallBecauseOfIdentityChangeBody
|
||||
//
|
||||
// let remotePhoneNumber = call.remotePhoneNumber
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber)
|
||||
//
|
||||
// guard let threadId = thread.uniqueId else {
|
||||
// owsFailDebug("threadId was unexpectedly nil")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let userInfo = [
|
||||
// AppNotificationUserInfoKey.threadId: threadId,
|
||||
// AppNotificationUserInfoKey.callBackNumber: remotePhoneNumber
|
||||
// ]
|
||||
//
|
||||
// DispatchQueue.main.async {
|
||||
// let sound = self.requestSound(thread: thread)
|
||||
// self.adaptee.notify(category: .missedCall,
|
||||
// title: notificationTitle,
|
||||
// body: notificationBody,
|
||||
// userInfo: userInfo,
|
||||
// sound: sound,
|
||||
// replacingIdentifier: call.localId.uuidString)
|
||||
// }
|
||||
// }
|
||||
|
||||
public func notifyUser(for incomingMessage: TSIncomingMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) {
|
||||
|
||||
guard !thread.isMuted else {
|
||||
|
@ -359,7 +213,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
// for more details.
|
||||
let messageText = DisplayableText.filterNotificationText(rawMessageText)
|
||||
|
||||
let senderName = OWSUserProfile.fetch(uniqueId: incomingMessage.authorId, transaction: transaction)?.profileName ?? contactsManager.displayName(forPhoneIdentifier: incomingMessage.authorId)
|
||||
let senderName = SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: incomingMessage.authorId, avoidingWriteTransaction: true) ?? incomingMessage.authorId
|
||||
|
||||
let notificationTitle: String?
|
||||
switch previewType {
|
||||
|
@ -401,12 +255,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
// Don't reply from lockscreen if anyone in this conversation is
|
||||
// "no longer verified".
|
||||
var category = AppNotificationCategory.incomingMessage
|
||||
for recipientId in thread.recipientIdentifiers {
|
||||
if self.identityManager.verificationState(forRecipientId: recipientId) == .noLongerVerified {
|
||||
category = AppNotificationCategory.incomingMessageFromNoLongerVerifiedIdentity
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let userInfo = [
|
||||
AppNotificationUserInfoKey.threadId: threadId
|
||||
|
@ -554,14 +402,6 @@ class NotificationActionHandler {
|
|||
return SignalApp.shared()
|
||||
}
|
||||
|
||||
var messageSender: MessageSender {
|
||||
return SSKEnvironment.shared.messageSender
|
||||
}
|
||||
|
||||
// var callUIAdapter: CallUIAdapter {
|
||||
// return AppEnvironment.shared.callService.callUIAdapter
|
||||
// }
|
||||
|
||||
var notificationPresenter: NotificationPresenter {
|
||||
return AppEnvironment.shared.notificationPresenter
|
||||
}
|
||||
|
@ -572,41 +412,6 @@ class NotificationActionHandler {
|
|||
|
||||
// MARK: -
|
||||
|
||||
// func answerCall(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
|
||||
// guard let localCallIdString = userInfo[AppNotificationUserInfoKey.localCallId] as? String else {
|
||||
// throw NotificationError.failDebug("localCallIdString was unexpectedly nil")
|
||||
// }
|
||||
//
|
||||
// guard let localCallId = UUID(uuidString: localCallIdString) else {
|
||||
// throw NotificationError.failDebug("unable to build localCallId. localCallIdString: \(localCallIdString)")
|
||||
// }
|
||||
//
|
||||
// callUIAdapter.answerCall(localId: localCallId)
|
||||
// return Promise.value(())
|
||||
// }
|
||||
//
|
||||
// func callBack(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
|
||||
// guard let recipientId = userInfo[AppNotificationUserInfoKey.callBackNumber] as? String else {
|
||||
// throw NotificationError.failDebug("recipientId was unexpectedly nil")
|
||||
// }
|
||||
//
|
||||
// callUIAdapter.startAndShowOutgoingCall(recipientId: recipientId, hasLocalVideo: false)
|
||||
// return Promise.value(())
|
||||
// }
|
||||
//
|
||||
// func declineCall(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
|
||||
// guard let localCallIdString = userInfo[AppNotificationUserInfoKey.localCallId] as? String else {
|
||||
// throw NotificationError.failDebug("localCallIdString was unexpectedly nil")
|
||||
// }
|
||||
//
|
||||
// guard let localCallId = UUID(uuidString: localCallIdString) else {
|
||||
// throw NotificationError.failDebug("unable to build localCallId. localCallIdString: \(localCallIdString)")
|
||||
// }
|
||||
//
|
||||
// callUIAdapter.declineCall(localId: localCallId)
|
||||
// return Promise.value(())
|
||||
// }
|
||||
|
||||
func markAsRead(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
|
||||
guard let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else {
|
||||
throw NotificationError.failDebug("threadId was unexpectedly nil")
|
||||
|
@ -629,15 +434,21 @@ class NotificationActionHandler {
|
|||
}
|
||||
|
||||
return markAsRead(thread: thread).then { () -> Promise<Void> in
|
||||
let sendPromise = ThreadUtil.sendMessageNonDurably(text: replyText,
|
||||
thread: thread,
|
||||
quotedReplyModel: nil,
|
||||
messageSender: self.messageSender)
|
||||
|
||||
return sendPromise.recover { error in
|
||||
Logger.warn("Failed to send reply message from notification with error: \(error)")
|
||||
self.notificationPresenter.notifyForFailedSend(inThread: thread)
|
||||
let message = VisibleMessage()
|
||||
message.sentTimestamp = NSDate.millisecondTimestamp()
|
||||
message.text = replyText
|
||||
let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread)
|
||||
Storage.write { transaction in
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
var promise: Promise<Void>!
|
||||
Storage.writeSync { transaction in
|
||||
promise = MessageSender.sendNonDurably(message, in: thread, using: transaction)
|
||||
}
|
||||
promise.catch { [weak self] error in
|
||||
self?.notificationPresenter.notifyForFailedSend(inThread: thread)
|
||||
}
|
||||
return promise
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -666,25 +477,6 @@ class NotificationActionHandler {
|
|||
}
|
||||
}
|
||||
|
||||
extension ThreadUtil {
|
||||
static var dbReadConnection: YapDatabaseConnection {
|
||||
return OWSPrimaryStorage.shared().dbReadConnection
|
||||
}
|
||||
|
||||
class func sendMessageNonDurably(text: String, thread: TSThread, quotedReplyModel: OWSQuotedReplyModel?, messageSender: MessageSender) -> Promise<Void> {
|
||||
return Promise { resolver in
|
||||
self.dbReadConnection.read { transaction in
|
||||
_ = self.sendMessageNonDurably(withText: text,
|
||||
in: thread,
|
||||
quotedReplyModel: quotedReplyModel,
|
||||
transaction: transaction,
|
||||
messageSender: messageSender,
|
||||
completion: resolver.resolve)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum NotificationError: Error {
|
||||
case assertionError(description: String)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ public class AvatarTableViewCell: UITableViewCell {
|
|||
|
||||
private let columns: UIStackView
|
||||
private let textRows: UIStackView
|
||||
private let avatarView: AvatarImageView
|
||||
// private let avatarView: AvatarImageView
|
||||
|
||||
private let _textLabel: UILabel
|
||||
override public var textLabel: UILabel? {
|
||||
|
@ -27,8 +27,8 @@ public class AvatarTableViewCell: UITableViewCell {
|
|||
|
||||
@objc
|
||||
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
self.avatarView = AvatarImageView()
|
||||
avatarView.autoSetDimensions(to: CGSize(width: CGFloat(kStandardAvatarSize), height: CGFloat(kStandardAvatarSize)))
|
||||
// self.avatarView = AvatarImageView()
|
||||
// avatarView.autoSetDimensions(to: CGSize(width: CGFloat(kStandardAvatarSize), height: CGFloat(kStandardAvatarSize)))
|
||||
|
||||
self._textLabel = UILabel()
|
||||
self._detailTextLabel = UILabel()
|
||||
|
@ -36,7 +36,7 @@ public class AvatarTableViewCell: UITableViewCell {
|
|||
self.textRows = UIStackView(arrangedSubviews: [_textLabel, _detailTextLabel])
|
||||
textRows.axis = .vertical
|
||||
|
||||
self.columns = UIStackView(arrangedSubviews: [avatarView, textRows])
|
||||
self.columns = UIStackView(arrangedSubviews: [ textRows ])
|
||||
columns.axis = .horizontal
|
||||
columns.spacing = CGFloat(kContactCellAvatarTextMargin)
|
||||
|
||||
|
@ -54,7 +54,7 @@ public class AvatarTableViewCell: UITableViewCell {
|
|||
|
||||
@objc
|
||||
public func configure(image: UIImage?, text: String?, detailText: String?) {
|
||||
self.avatarView.image = image
|
||||
// self.avatarView.image = image
|
||||
self.textLabel?.text = text
|
||||
self.detailTextLabel?.text = detailText
|
||||
|
||||
|
@ -65,7 +65,7 @@ public class AvatarTableViewCell: UITableViewCell {
|
|||
public override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
self.avatarView.image = nil
|
||||
// self.avatarView.image = nil
|
||||
self.textLabel?.text = nil
|
||||
self.detailTextLabel?.text = nil
|
||||
}
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
#import "OWSNavigationController.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <MobileCoreServices/UTCoreTypes.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/PhoneNumber.h>
|
||||
#import <SignalUtilitiesKit/TSGroupModel.h>
|
||||
#import <SignalUtilitiesKit/TSGroupThread.h>
|
||||
#import <SignalUtilitiesKit/TSThread.h>
|
||||
|
||||
#import <SessionMessagingKit/TSGroupModel.h>
|
||||
#import <SessionMessagingKit/TSGroupThread.h>
|
||||
#import <SessionMessagingKit/TSThread.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
@ -36,14 +36,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
[actionSheet addAction:[OWSAlerts cancelAction]];
|
||||
|
||||
// UIAlertAction *takePictureAction = [UIAlertAction
|
||||
// actionWithTitle:NSLocalizedString(@"MEDIA_FROM_CAMERA_BUTTON", @"media picker option to take photo or video")
|
||||
// style:UIAlertActionStyleDefault
|
||||
// handler:^(UIAlertAction *_Nonnull action) {
|
||||
// [self takePicture];
|
||||
// }];
|
||||
// [actionSheet addAction:takePictureAction];
|
||||
|
||||
UIAlertAction *choosePictureAction = [UIAlertAction
|
||||
actionWithTitle:NSLocalizedString(@"MEDIA_FROM_LIBRARY_BUTTON", @"media picker option to choose from library")
|
||||
style:UIAlertActionStyleDefault
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface BlockListViewController : OWSViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,175 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BlockListViewController.h"
|
||||
#import "AddToBlockListViewController.h"
|
||||
#import "BlockListUIUtils.h"
|
||||
#import "ContactTableViewCell.h"
|
||||
#import "ContactsViewHelper.h"
|
||||
#import "OWSTableViewController.h"
|
||||
#import "PhoneNumber.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/OWSBlockingManager.h>
|
||||
#import <SignalUtilitiesKit/TSGroupThread.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface BlockListViewController () <ContactsViewHelperDelegate>
|
||||
|
||||
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
|
||||
|
||||
@property (nonatomic, readonly) OWSTableViewController *tableViewController;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation BlockListViewController
|
||||
|
||||
- (OWSBlockingManager *)blockingManager
|
||||
{
|
||||
return OWSBlockingManager.sharedManager;
|
||||
}
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
_contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
|
||||
|
||||
self.title
|
||||
= NSLocalizedString(@"SETTINGS_BLOCK_LIST_TITLE", @"Label for the block list section of the settings view");
|
||||
|
||||
_tableViewController = [OWSTableViewController new];
|
||||
[self.view addSubview:self.tableViewController.view];
|
||||
[self addChildViewController:self.tableViewController];
|
||||
[_tableViewController.view autoPinEdgesToSuperviewEdges];
|
||||
self.tableViewController.tableView.rowHeight = UITableViewAutomaticDimension;
|
||||
self.tableViewController.tableView.estimatedRowHeight = 60;
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - Table view data source
|
||||
|
||||
- (void)updateTableContents
|
||||
{
|
||||
OWSTableContents *contents = [OWSTableContents new];
|
||||
|
||||
__weak BlockListViewController *weakSelf = self;
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
|
||||
// "Add" section
|
||||
|
||||
OWSTableSection *addSection = [OWSTableSection new];
|
||||
addSection.footerTitle = NSLocalizedString(
|
||||
@"BLOCK_USER_BEHAVIOR_EXPLANATION", @"An explanation of the consequences of blocking another user.");
|
||||
|
||||
[addSection
|
||||
addItem:[OWSTableItem
|
||||
disclosureItemWithText:NSLocalizedString(@"SETTINGS_BLOCK_LIST_ADD_BUTTON",
|
||||
@"A label for the 'add phone number' button in the block list table.")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"add")
|
||||
actionBlock:^{
|
||||
AddToBlockListViewController *vc = [[AddToBlockListViewController alloc] init];
|
||||
[weakSelf.navigationController pushViewController:vc animated:YES];
|
||||
}]];
|
||||
[contents addSection:addSection];
|
||||
|
||||
// "Blocklist" section
|
||||
|
||||
NSArray<NSString *> *blockedPhoneNumbers =
|
||||
[self.blockingManager.blockedPhoneNumbers sortedArrayUsingSelector:@selector(compare:)];
|
||||
|
||||
if (blockedPhoneNumbers.count > 0) {
|
||||
OWSTableSection *blockedContactsSection = [OWSTableSection new];
|
||||
blockedContactsSection.headerTitle = NSLocalizedString(
|
||||
@"BLOCK_LIST_BLOCKED_USERS_SECTION", @"Section header for users that have been blocked");
|
||||
|
||||
for (NSString *phoneNumber in blockedPhoneNumbers) {
|
||||
[blockedContactsSection addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
ContactTableViewCell *cell = [ContactTableViewCell new];
|
||||
[cell configureWithRecipientId:phoneNumber];
|
||||
cell.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(
|
||||
BlockListViewController, @"user");
|
||||
return cell;
|
||||
}
|
||||
customRowHeight:UITableViewAutomaticDimension
|
||||
actionBlock:^{
|
||||
[BlockListUIUtils
|
||||
showUnblockPhoneNumberActionSheet:phoneNumber
|
||||
fromViewController:weakSelf
|
||||
blockingManager:helper.blockingManager
|
||||
contactsManager:helper.contactsManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
[weakSelf updateTableContents];
|
||||
}];
|
||||
}]];
|
||||
}
|
||||
[contents addSection:blockedContactsSection];
|
||||
}
|
||||
|
||||
NSArray<TSGroupModel *> *blockedGroups = self.blockingManager.blockedGroups;
|
||||
if (blockedGroups.count > 0) {
|
||||
OWSTableSection *blockedGroupsSection = [OWSTableSection new];
|
||||
blockedGroupsSection.headerTitle = NSLocalizedString(
|
||||
@"BLOCK_LIST_BLOCKED_GROUPS_SECTION", @"Section header for groups that have been blocked");
|
||||
|
||||
for (TSGroupModel *blockedGroup in blockedGroups) {
|
||||
UIImage *_Nullable image = blockedGroup.groupImage;
|
||||
if (!image) {
|
||||
NSString *conversationColorName =
|
||||
[TSGroupThread defaultConversationColorNameForGroupId:blockedGroup.groupId];
|
||||
image = [OWSGroupAvatarBuilder defaultAvatarForGroupId:blockedGroup.groupId
|
||||
conversationColorName:conversationColorName
|
||||
diameter:kStandardAvatarSize];
|
||||
}
|
||||
NSString *groupName
|
||||
= blockedGroup.groupName.length > 0 ? blockedGroup.groupName : TSGroupThread.defaultGroupName;
|
||||
|
||||
[blockedGroupsSection addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
OWSAvatarTableViewCell *cell = [OWSAvatarTableViewCell new];
|
||||
[cell configureWithImage:image
|
||||
text:groupName
|
||||
detailText:nil];
|
||||
return cell;
|
||||
}
|
||||
customRowHeight:UITableViewAutomaticDimension
|
||||
actionBlock:^{
|
||||
[BlockListUIUtils showUnblockGroupActionSheet:blockedGroup
|
||||
displayName:groupName
|
||||
fromViewController:weakSelf
|
||||
blockingManager:helper.blockingManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
[weakSelf updateTableContents];
|
||||
}];
|
||||
}]];
|
||||
}
|
||||
[contents addSection:blockedGroupsSection];
|
||||
}
|
||||
|
||||
self.tableViewController.contents = contents;
|
||||
}
|
||||
|
||||
#pragma mark - ContactsViewHelperDelegate
|
||||
|
||||
- (void)contactsViewHelperDidUpdateContacts
|
||||
{
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (BOOL)shouldHideLocalNumber
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,549 +0,0 @@
|
|||
////
|
||||
//// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
////
|
||||
//
|
||||
//import Foundation
|
||||
//import AVFoundation
|
||||
//import SignalUtilitiesKit
|
||||
//import SignalUtilitiesKit
|
||||
//
|
||||
//struct AudioSource: Hashable {
|
||||
//
|
||||
// let image: UIImage
|
||||
// let localizedName: String
|
||||
// let portDescription: AVAudioSessionPortDescription?
|
||||
//
|
||||
// // The built-in loud speaker / aka speakerphone
|
||||
// let isBuiltInSpeaker: Bool
|
||||
//
|
||||
// // The built-in quiet speaker, aka the normal phone handset receiver earpiece
|
||||
// let isBuiltInEarPiece: Bool
|
||||
//
|
||||
// init(localizedName: String, image: UIImage, isBuiltInSpeaker: Bool, isBuiltInEarPiece: Bool, portDescription: AVAudioSessionPortDescription? = nil) {
|
||||
// self.localizedName = localizedName
|
||||
// self.image = image
|
||||
// self.isBuiltInSpeaker = isBuiltInSpeaker
|
||||
// self.isBuiltInEarPiece = isBuiltInEarPiece
|
||||
// self.portDescription = portDescription
|
||||
// }
|
||||
//
|
||||
// init(portDescription: AVAudioSessionPortDescription) {
|
||||
//
|
||||
// let isBuiltInEarPiece = portDescription.portType == AVAudioSession.Port.builtInMic
|
||||
//
|
||||
// // portDescription.portName works well for BT linked devices, but if we are using
|
||||
// // the built in mic, we have "iPhone Microphone" which is a little awkward.
|
||||
// // In that case, instead we prefer just the model name e.g. "iPhone" or "iPad"
|
||||
// let localizedName = isBuiltInEarPiece ? UIDevice.current.localizedModel : portDescription.portName
|
||||
//
|
||||
// self.init(localizedName: localizedName,
|
||||
// image: #imageLiteral(resourceName: "button_phone_white"), // TODO
|
||||
// isBuiltInSpeaker: false,
|
||||
// isBuiltInEarPiece: isBuiltInEarPiece,
|
||||
// portDescription: portDescription)
|
||||
// }
|
||||
//
|
||||
// // Speakerphone is handled separately from the other audio routes as it doesn't appear as an "input"
|
||||
// static var builtInSpeaker: AudioSource {
|
||||
// return self.init(localizedName: NSLocalizedString("AUDIO_ROUTE_BUILT_IN_SPEAKER", comment: "action sheet button title to enable built in speaker during a call"),
|
||||
// image: #imageLiteral(resourceName: "button_phone_white"), //TODO
|
||||
// isBuiltInSpeaker: true,
|
||||
// isBuiltInEarPiece: false)
|
||||
// }
|
||||
//
|
||||
// // MARK: Hashable
|
||||
//
|
||||
// static func ==(lhs: AudioSource, rhs: AudioSource) -> Bool {
|
||||
// // Simply comparing the `portDescription` vs the `portDescription.uid`
|
||||
// // caused multiple instances of the built in mic to turn up in a set.
|
||||
// if lhs.isBuiltInSpeaker && rhs.isBuiltInSpeaker {
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// if lhs.isBuiltInSpeaker || rhs.isBuiltInSpeaker {
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// guard let lhsPortDescription = lhs.portDescription else {
|
||||
// owsFailDebug("only the built in speaker should lack a port description")
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// guard let rhsPortDescription = rhs.portDescription else {
|
||||
// owsFailDebug("only the built in speaker should lack a port description")
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// return lhsPortDescription.uid == rhsPortDescription.uid
|
||||
// }
|
||||
//
|
||||
// var hashValue: Int {
|
||||
// guard let portDescription = self.portDescription else {
|
||||
// assert(self.isBuiltInSpeaker)
|
||||
// return "Built In Speaker".hashValue
|
||||
// }
|
||||
// return portDescription.uid.hash
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//protocol CallAudioServiceDelegate: class {
|
||||
// func callAudioService(_ callAudioService: CallAudioService, didUpdateIsSpeakerphoneEnabled isEnabled: Bool)
|
||||
// func callAudioServiceDidChangeAudioSession(_ callAudioService: CallAudioService)
|
||||
//}
|
||||
//
|
||||
//@objc class CallAudioService: NSObject, CallObserver {
|
||||
//
|
||||
// private var vibrateTimer: Timer?
|
||||
// private let audioPlayer = AVAudioPlayer()
|
||||
// private let handleRinging: Bool
|
||||
// weak var delegate: CallAudioServiceDelegate? {
|
||||
// willSet {
|
||||
// assert(newValue == nil || delegate == nil)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // MARK: Vibration config
|
||||
// private let vibrateRepeatDuration = 1.6
|
||||
//
|
||||
// // Our ring buzz is a pair of vibrations.
|
||||
// // `pulseDuration` is the small pause between the two vibrations in the pair.
|
||||
// private let pulseDuration = 0.2
|
||||
//
|
||||
// var audioSession: OWSAudioSession {
|
||||
// return Environment.shared.audioSession
|
||||
// }
|
||||
//
|
||||
// var avAudioSession: AVAudioSession {
|
||||
// return AVAudioSession.sharedInstance()
|
||||
// }
|
||||
//
|
||||
// // MARK: - Initializers
|
||||
//
|
||||
// init(handleRinging: Bool) {
|
||||
// self.handleRinging = handleRinging
|
||||
//
|
||||
// super.init()
|
||||
//
|
||||
// // We cannot assert singleton here, because this class gets rebuilt when the user changes relevant call settings
|
||||
//
|
||||
// // Configure audio session so we don't prompt user with Record permission until call is connected.
|
||||
//
|
||||
// audioSession.configureRTCAudio()
|
||||
// NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: avAudioSession, queue: nil) { _ in
|
||||
// assert(!Thread.isMainThread)
|
||||
// self.updateIsSpeakerphoneEnabled()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// deinit {
|
||||
// NotificationCenter.default.removeObserver(self)
|
||||
// }
|
||||
//
|
||||
// // MARK: - CallObserver
|
||||
//
|
||||
// internal func stateDidChange(call: SignalCall, state: CallState) {
|
||||
// AssertIsOnMainThread()
|
||||
// self.handleState(call: call)
|
||||
// }
|
||||
//
|
||||
// internal func muteDidChange(call: SignalCall, isMuted: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// ensureProperAudioSession(call: call)
|
||||
// }
|
||||
//
|
||||
// internal func holdDidChange(call: SignalCall, isOnHold: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// ensureProperAudioSession(call: call)
|
||||
// }
|
||||
//
|
||||
// internal func audioSourceDidChange(call: SignalCall, audioSource: AudioSource?) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// ensureProperAudioSession(call: call)
|
||||
//
|
||||
// if let audioSource = audioSource, audioSource.isBuiltInSpeaker {
|
||||
// self.isSpeakerphoneEnabled = true
|
||||
// } else {
|
||||
// self.isSpeakerphoneEnabled = false
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// internal func hasLocalVideoDidChange(call: SignalCall, hasLocalVideo: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// ensureProperAudioSession(call: call)
|
||||
// }
|
||||
//
|
||||
// // Speakerphone can be manipulated by the in-app callscreen or via the system callscreen (CallKit).
|
||||
// // Unlike other CallKit CallScreen buttons, enabling doesn't trigger a CXAction, so it's not as simple
|
||||
// // to track state changes. Instead we never store the state and directly access the ground-truth in the
|
||||
// // AVAudioSession.
|
||||
// private(set) var isSpeakerphoneEnabled: Bool = false {
|
||||
// didSet {
|
||||
// self.delegate?.callAudioService(self, didUpdateIsSpeakerphoneEnabled: isSpeakerphoneEnabled)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public func requestSpeakerphone(isEnabled: Bool) {
|
||||
// // This is a little too slow to execute on the main thread and the results are not immediately available after execution
|
||||
// // anyway, so we dispatch async. If you need to know the new value, you'll need to check isSpeakerphoneEnabled and take
|
||||
// // advantage of the CallAudioServiceDelegate.callAudioService(_:didUpdateIsSpeakerphoneEnabled:)
|
||||
// DispatchQueue.global().async {
|
||||
// do {
|
||||
// try self.avAudioSession.overrideOutputAudioPort( isEnabled ? .speaker : .none )
|
||||
// } catch {
|
||||
// owsFailDebug("failed to set \(#function) = \(isEnabled) with error: \(error)")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func updateIsSpeakerphoneEnabled() {
|
||||
// let value = avAudioSession.currentRoute.outputs.contains { (portDescription: AVAudioSessionPortDescription) -> Bool in
|
||||
// return portDescription.portType == .builtInSpeaker
|
||||
// }
|
||||
// DispatchQueue.main.async {
|
||||
// self.isSpeakerphoneEnabled = value
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func ensureProperAudioSession(call: SignalCall?) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// guard let call = call, !call.isTerminated else {
|
||||
// // Revert to default audio
|
||||
// setAudioSession(category: .soloAmbient,
|
||||
// mode: .default)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Disallow bluetooth while (and only while) the user has explicitly chosen the built in receiver.
|
||||
// //
|
||||
// // NOTE: I'm actually not sure why this is required - it seems like we should just be able
|
||||
// // to setPreferredInput to call.audioSource.portDescription in this case,
|
||||
// // but in practice I'm seeing the call revert to the bluetooth headset.
|
||||
// // Presumably something else (in WebRTC?) is touching our shared AudioSession. - mjk
|
||||
// let options: AVAudioSession.CategoryOptions = call.audioSource?.isBuiltInEarPiece == true ? [] : [.allowBluetooth]
|
||||
//
|
||||
// if call.state == .localRinging {
|
||||
// // SoloAmbient plays through speaker, but respects silent switch
|
||||
// setAudioSession(category: .soloAmbient,
|
||||
// mode: .default)
|
||||
// } else if call.hasLocalVideo {
|
||||
// // Because ModeVideoChat affects gain, we don't want to apply it until the call is connected.
|
||||
// // otherwise sounds like ringing will be extra loud for video vs. speakerphone
|
||||
//
|
||||
// // Apple Docs say that setting mode to AVAudioSessionModeVideoChat has the
|
||||
// // side effect of setting options: .allowBluetooth, when I remove the (seemingly unnecessary)
|
||||
// // option, and inspect AVAudioSession.sharedInstance.categoryOptions == 0. And availableInputs
|
||||
// // does not include my linked bluetooth device
|
||||
// setAudioSession(category: .playAndRecord,
|
||||
// mode: .videoChat,
|
||||
// options: options)
|
||||
// } else {
|
||||
// // Apple Docs say that setting mode to AVAudioSessionModeVoiceChat has the
|
||||
// // side effect of setting options: .allowBluetooth, when I remove the (seemingly unnecessary)
|
||||
// // option, and inspect AVAudioSession.sharedInstance.categoryOptions == 0. And availableInputs
|
||||
// // does not include my linked bluetooth device
|
||||
// setAudioSession(category: .playAndRecord,
|
||||
// mode: .voiceChat,
|
||||
// options: options)
|
||||
// }
|
||||
//
|
||||
// do {
|
||||
// // It's important to set preferred input *after* ensuring properAudioSession
|
||||
// // because some sources are only valid for certain category/option combinations.
|
||||
// let existingPreferredInput = avAudioSession.preferredInput
|
||||
// if existingPreferredInput != call.audioSource?.portDescription {
|
||||
// Logger.info("changing preferred input: \(String(describing: existingPreferredInput)) -> \(String(describing: call.audioSource?.portDescription))")
|
||||
// try avAudioSession.setPreferredInput(call.audioSource?.portDescription)
|
||||
// }
|
||||
//
|
||||
// } catch {
|
||||
// owsFailDebug("failed setting audio source with error: \(error) isSpeakerPhoneEnabled: \(call.isSpeakerphoneEnabled)")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // MARK: - Service action handlers
|
||||
//
|
||||
// public func didUpdateVideoTracks(call: SignalCall?) {
|
||||
// Logger.verbose("")
|
||||
//
|
||||
// self.ensureProperAudioSession(call: call)
|
||||
// }
|
||||
//
|
||||
// public func handleState(call: SignalCall) {
|
||||
// assert(Thread.isMainThread)
|
||||
//
|
||||
// Logger.verbose("new state: \(call.state)")
|
||||
//
|
||||
// // Stop playing sounds while switching audio session so we don't
|
||||
// // get any blips across a temporary unintended route.
|
||||
// stopPlayingAnySounds()
|
||||
// self.ensureProperAudioSession(call: call)
|
||||
//
|
||||
// switch call.state {
|
||||
// case .idle: handleIdle(call: call)
|
||||
// case .dialing: handleDialing(call: call)
|
||||
// case .answering: handleAnswering(call: call)
|
||||
// case .remoteRinging: handleRemoteRinging(call: call)
|
||||
// case .localRinging: handleLocalRinging(call: call)
|
||||
// case .connected: handleConnected(call: call)
|
||||
// case .reconnecting: handleReconnecting(call: call)
|
||||
// case .localFailure: handleLocalFailure(call: call)
|
||||
// case .localHangup: handleLocalHangup(call: call)
|
||||
// case .remoteHangup: handleRemoteHangup(call: call)
|
||||
// case .remoteBusy: handleBusy(call: call)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func handleIdle(call: SignalCall) {
|
||||
// Logger.debug("")
|
||||
// }
|
||||
//
|
||||
// private func handleDialing(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// // HACK: Without this async, dialing sound only plays once. I don't really understand why. Does the audioSession
|
||||
// // need some time to settle? Is somethign else interrupting our session?
|
||||
// DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) {
|
||||
// self.play(sound: OWSSound.callConnecting)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func handleAnswering(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
// }
|
||||
//
|
||||
// private func handleRemoteRinging(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// self.play(sound: OWSSound.callOutboundRinging)
|
||||
// }
|
||||
//
|
||||
// private func handleLocalRinging(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// startRinging(call: call)
|
||||
// }
|
||||
//
|
||||
// private func handleConnected(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
// }
|
||||
//
|
||||
// private func handleReconnecting(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
// }
|
||||
//
|
||||
// private func handleLocalFailure(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// play(sound: OWSSound.callFailure)
|
||||
// handleCallEnded(call: call)
|
||||
// }
|
||||
//
|
||||
// private func handleLocalHangup(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// handleCallEnded(call: call)
|
||||
// }
|
||||
//
|
||||
// private func handleRemoteHangup(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// vibrate()
|
||||
//
|
||||
// handleCallEnded(call: call)
|
||||
// }
|
||||
//
|
||||
// private func handleBusy(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// play(sound: OWSSound.callBusy)
|
||||
//
|
||||
// // Let the busy sound play for 4 seconds. The full file is longer than necessary
|
||||
// DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 4.0) {
|
||||
// self.handleCallEnded(call: call)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func handleCallEnded(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// // Stop solo audio, revert to default.
|
||||
// isSpeakerphoneEnabled = false
|
||||
// setAudioSession(category: .soloAmbient)
|
||||
// }
|
||||
//
|
||||
// // MARK: Playing Sounds
|
||||
//
|
||||
// var currentPlayer: OWSAudioPlayer?
|
||||
//
|
||||
// private func stopPlayingAnySounds() {
|
||||
// currentPlayer?.stop()
|
||||
// stopAnyRingingVibration()
|
||||
// }
|
||||
//
|
||||
// private func play(sound: OWSSound) {
|
||||
// guard let newPlayer = OWSSounds.audioPlayer(for: sound, audioBehavior: .call) else {
|
||||
// owsFailDebug("unable to build player for sound: \(OWSSounds.displayName(for: sound))")
|
||||
// return
|
||||
// }
|
||||
// Logger.info("playing sound: \(OWSSounds.displayName(for: sound))")
|
||||
//
|
||||
// // It's important to stop the current player **before** starting the new player. In the case that
|
||||
// // we're playing the same sound, since the player is memoized on the sound instance, we'd otherwise
|
||||
// // stop the sound we just started.
|
||||
// self.currentPlayer?.stop()
|
||||
// newPlayer.play()
|
||||
// self.currentPlayer = newPlayer
|
||||
// }
|
||||
//
|
||||
// // MARK: - Ringing
|
||||
//
|
||||
// private func startRinging(call: SignalCall) {
|
||||
// guard handleRinging else {
|
||||
// Logger.debug("ignoring \(#function) since CallKit handles it's own ringing state")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// vibrateTimer = WeakTimer.scheduledTimer(timeInterval: vibrateRepeatDuration, target: self, userInfo: nil, repeats: true) {[weak self] _ in
|
||||
// self?.ringVibration()
|
||||
// }
|
||||
// vibrateTimer?.fire()
|
||||
// play(sound: .defaultiOSIncomingRingtone)
|
||||
// }
|
||||
//
|
||||
// private func stopAnyRingingVibration() {
|
||||
// guard handleRinging else {
|
||||
// Logger.debug("ignoring \(#function) since CallKit handles it's own ringing state")
|
||||
// return
|
||||
// }
|
||||
// Logger.debug("")
|
||||
//
|
||||
// // Stop vibrating
|
||||
// vibrateTimer?.invalidate()
|
||||
// vibrateTimer = nil
|
||||
// }
|
||||
//
|
||||
// // public so it can be called by timer via selector
|
||||
// public func ringVibration() {
|
||||
// // Since a call notification is more urgent than a message notifaction, we
|
||||
// // vibrate twice, like a pulse, to differentiate from a normal notification vibration.
|
||||
// vibrate()
|
||||
// DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + pulseDuration) {
|
||||
// self.vibrate()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func vibrate() {
|
||||
// // TODO implement HapticAdapter for iPhone7 and up
|
||||
// AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
|
||||
// }
|
||||
//
|
||||
// // MARK: - AudioSession MGMT
|
||||
// // TODO move this to CallAudioSession?
|
||||
//
|
||||
// // Note this method is sensitive to the current audio session configuration.
|
||||
// // Specifically if you call it while speakerphone is enabled you won't see
|
||||
// // any connected bluetooth routes.
|
||||
// var availableInputs: [AudioSource] {
|
||||
// guard let availableInputs = avAudioSession.availableInputs else {
|
||||
// // I'm not sure why this would happen, but it may indicate an error.
|
||||
// owsFailDebug("No available inputs or inputs not ready")
|
||||
// return [AudioSource.builtInSpeaker]
|
||||
// }
|
||||
//
|
||||
// Logger.info("availableInputs: \(availableInputs)")
|
||||
// return [AudioSource.builtInSpeaker] + availableInputs.map { portDescription in
|
||||
// return AudioSource(portDescription: portDescription)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func currentAudioSource(call: SignalCall) -> AudioSource? {
|
||||
// if let audioSource = call.audioSource {
|
||||
// return audioSource
|
||||
// }
|
||||
//
|
||||
// // Before the user has specified an audio source on the call, we rely on the existing
|
||||
// // system state to determine the current audio source.
|
||||
// // If a bluetooth is connected, this will be bluetooth, otherwise
|
||||
// // this will be the receiver.
|
||||
// guard let portDescription = avAudioSession.currentRoute.inputs.first else {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// return AudioSource(portDescription: portDescription)
|
||||
// }
|
||||
//
|
||||
// private func setAudioSession(category: AVAudioSession.Category,
|
||||
// mode: AVAudioSession.Mode? = nil,
|
||||
// options: AVAudioSession.CategoryOptions = AVAudioSession.CategoryOptions(rawValue: 0)) {
|
||||
//
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// var audioSessionChanged = false
|
||||
// do {
|
||||
// if #available(iOS 10.0, *), let mode = mode {
|
||||
// let oldCategory = avAudioSession.category
|
||||
// let oldMode = avAudioSession.mode
|
||||
// let oldOptions = avAudioSession.categoryOptions
|
||||
//
|
||||
// guard oldCategory != category || oldMode != mode || oldOptions != options else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// audioSessionChanged = true
|
||||
//
|
||||
// if oldCategory != category {
|
||||
// Logger.debug("audio session changed category: \(oldCategory) -> \(category) ")
|
||||
// }
|
||||
// if oldMode != mode {
|
||||
// Logger.debug("audio session changed mode: \(oldMode) -> \(mode) ")
|
||||
// }
|
||||
// if oldOptions != options {
|
||||
// Logger.debug("audio session changed options: \(oldOptions) -> \(options) ")
|
||||
// }
|
||||
// try avAudioSession.setCategory(category, mode: mode, options: options)
|
||||
//
|
||||
// } else {
|
||||
// let oldCategory = avAudioSession.category
|
||||
// let oldOptions = avAudioSession.categoryOptions
|
||||
//
|
||||
// guard avAudioSession.category != category || avAudioSession.categoryOptions != options else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// audioSessionChanged = true
|
||||
//
|
||||
// if oldCategory != category {
|
||||
// Logger.debug("audio session changed category: \(oldCategory) -> \(category) ")
|
||||
// }
|
||||
// if oldOptions != options {
|
||||
// Logger.debug("audio session changed options: \(oldOptions) -> \(options) ")
|
||||
// }
|
||||
// try avAudioSession.ows_setCategory(category, with: options)
|
||||
// }
|
||||
// } catch {
|
||||
// let message = "failed to set category: \(category) mode: \(String(describing: mode)), options: \(options) with error: \(error)"
|
||||
// owsFailDebug(message)
|
||||
// }
|
||||
//
|
||||
// if audioSessionChanged {
|
||||
// Logger.info("")
|
||||
// self.delegate?.callAudioServiceDidChangeAudioSession(self)
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -1,133 +0,0 @@
|
|||
////
|
||||
//// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
////
|
||||
//
|
||||
//import UIKit
|
||||
//import CallKit
|
||||
//import SignalUtilitiesKit
|
||||
//
|
||||
///**
|
||||
// * Requests actions from CallKit
|
||||
// *
|
||||
// * @Discussion:
|
||||
// * Based on SpeakerboxCallManager, from the Apple CallKit Example app. Though, it's responsibilities are mostly
|
||||
// * mirrored (and delegated from) CallKitCallUIAdaptee.
|
||||
// * TODO: Would it simplify things to merge this into CallKitCallUIAdaptee?
|
||||
// */
|
||||
//@available(iOS 10.0, *)
|
||||
//final class CallKitCallManager: NSObject {
|
||||
//
|
||||
// let callController = CXCallController()
|
||||
// let showNamesOnCallScreen: Bool
|
||||
//
|
||||
// @objc
|
||||
// static let kAnonymousCallHandlePrefix = "Signal:"
|
||||
//
|
||||
// required init(showNamesOnCallScreen: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// self.showNamesOnCallScreen = showNamesOnCallScreen
|
||||
// super.init()
|
||||
//
|
||||
// // We cannot assert singleton here, because this class gets rebuilt when the user changes relevant call settings
|
||||
// }
|
||||
//
|
||||
// // MARK: Actions
|
||||
//
|
||||
// func startCall(_ call: SignalCall) {
|
||||
// var handle: CXHandle
|
||||
//
|
||||
// if showNamesOnCallScreen {
|
||||
// handle = CXHandle(type: .phoneNumber, value: call.remotePhoneNumber)
|
||||
// } else {
|
||||
// let callKitId = CallKitCallManager.kAnonymousCallHandlePrefix + call.localId.uuidString
|
||||
// handle = CXHandle(type: .generic, value: callKitId)
|
||||
// OWSPrimaryStorage.shared().setPhoneNumber(call.remotePhoneNumber, forCallKitId: callKitId)
|
||||
// }
|
||||
//
|
||||
// let startCallAction = CXStartCallAction(call: call.localId, handle: handle)
|
||||
//
|
||||
// startCallAction.isVideo = call.hasLocalVideo
|
||||
//
|
||||
// let transaction = CXTransaction()
|
||||
// transaction.addAction(startCallAction)
|
||||
//
|
||||
// requestTransaction(transaction)
|
||||
// }
|
||||
//
|
||||
// func localHangup(call: SignalCall) {
|
||||
// let endCallAction = CXEndCallAction(call: call.localId)
|
||||
// let transaction = CXTransaction()
|
||||
// transaction.addAction(endCallAction)
|
||||
//
|
||||
// requestTransaction(transaction)
|
||||
// }
|
||||
//
|
||||
// func setHeld(call: SignalCall, onHold: Bool) {
|
||||
// let setHeldCallAction = CXSetHeldCallAction(call: call.localId, onHold: onHold)
|
||||
// let transaction = CXTransaction()
|
||||
// transaction.addAction(setHeldCallAction)
|
||||
//
|
||||
// requestTransaction(transaction)
|
||||
// }
|
||||
//
|
||||
// func setIsMuted(call: SignalCall, isMuted: Bool) {
|
||||
// let muteCallAction = CXSetMutedCallAction(call: call.localId, muted: isMuted)
|
||||
// let transaction = CXTransaction()
|
||||
// transaction.addAction(muteCallAction)
|
||||
//
|
||||
// requestTransaction(transaction)
|
||||
// }
|
||||
//
|
||||
// func answer(call: SignalCall) {
|
||||
// let answerCallAction = CXAnswerCallAction(call: call.localId)
|
||||
// let transaction = CXTransaction()
|
||||
// transaction.addAction(answerCallAction)
|
||||
//
|
||||
// requestTransaction(transaction)
|
||||
// }
|
||||
//
|
||||
// private func requestTransaction(_ transaction: CXTransaction) {
|
||||
// callController.request(transaction) { error in
|
||||
// if let error = error {
|
||||
// Logger.error("Error requesting transaction: \(error)")
|
||||
// } else {
|
||||
// Logger.debug("Requested transaction successfully")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // MARK: Call Management
|
||||
//
|
||||
// private(set) var calls = [SignalCall]()
|
||||
//
|
||||
// func callWithLocalId(_ localId: UUID) -> SignalCall? {
|
||||
// guard let index = calls.firstIndex(where: { $0.localId == localId }) else {
|
||||
// return nil
|
||||
// }
|
||||
// return calls[index]
|
||||
// }
|
||||
//
|
||||
// func addCall(_ call: SignalCall) {
|
||||
// calls.append(call)
|
||||
// }
|
||||
//
|
||||
// func removeCall(_ call: SignalCall) {
|
||||
// calls.removeFirst(where: { $0 === call })
|
||||
// }
|
||||
//
|
||||
// func removeAllCalls() {
|
||||
// calls.removeAll()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//fileprivate extension Array {
|
||||
//
|
||||
// mutating func removeFirst(where predicate: (Element) throws -> Bool) rethrows {
|
||||
// guard let index = try firstIndex(where: predicate) else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// remove(at: index)
|
||||
// }
|
||||
//}
|
|
@ -1,414 +0,0 @@
|
|||
////
|
||||
//// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
////
|
||||
//
|
||||
//import Foundation
|
||||
//import UIKit
|
||||
//import CallKit
|
||||
//import AVFoundation
|
||||
//import SignalUtilitiesKit
|
||||
//import SignalUtilitiesKit
|
||||
//
|
||||
///**
|
||||
// * Connects user interface to the CallService using CallKit.
|
||||
// *
|
||||
// * User interface is routed to the CallManager which requests CXCallActions, and if the CXProvider accepts them,
|
||||
// * their corresponding consequences are implmented in the CXProviderDelegate methods, e.g. using the CallService
|
||||
// */
|
||||
//@available(iOS 10.0, *)
|
||||
//final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
|
||||
//
|
||||
// private let callManager: CallKitCallManager
|
||||
// internal let callService: CallService
|
||||
// internal let notificationPresenter: NotificationPresenter
|
||||
// internal let contactsManager: OWSContactsManager
|
||||
// private let showNamesOnCallScreen: Bool
|
||||
// private let provider: CXProvider
|
||||
// private let audioActivity: AudioActivity
|
||||
//
|
||||
// // CallKit handles incoming ringer stop/start for us. Yay!
|
||||
// let hasManualRinger = false
|
||||
//
|
||||
// // Instantiating more than one CXProvider can cause us to miss call transactions, so
|
||||
// // we maintain the provider across Adaptees using a singleton pattern
|
||||
// private static var _sharedProvider: CXProvider?
|
||||
// class func sharedProvider(useSystemCallLog: Bool) -> CXProvider {
|
||||
// let configuration = buildProviderConfiguration(useSystemCallLog: useSystemCallLog)
|
||||
//
|
||||
// if let sharedProvider = self._sharedProvider {
|
||||
// sharedProvider.configuration = configuration
|
||||
// return sharedProvider
|
||||
// } else {
|
||||
// SwiftSingletons.register(self)
|
||||
// let provider = CXProvider(configuration: configuration)
|
||||
// _sharedProvider = provider
|
||||
// return provider
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // The app's provider configuration, representing its CallKit capabilities
|
||||
// class func buildProviderConfiguration(useSystemCallLog: Bool) -> CXProviderConfiguration {
|
||||
// let localizedName = NSLocalizedString("APPLICATION_NAME", comment: "Name of application")
|
||||
// let providerConfiguration = CXProviderConfiguration(localizedName: localizedName)
|
||||
//
|
||||
// providerConfiguration.supportsVideo = true
|
||||
//
|
||||
// providerConfiguration.maximumCallGroups = 1
|
||||
//
|
||||
// providerConfiguration.maximumCallsPerCallGroup = 1
|
||||
//
|
||||
// providerConfiguration.supportedHandleTypes = [.phoneNumber, .generic]
|
||||
//
|
||||
// let iconMaskImage = #imageLiteral(resourceName: "logoSignal")
|
||||
// providerConfiguration.iconTemplateImageData = iconMaskImage.pngData()
|
||||
//
|
||||
// // We don't set the ringtoneSound property, so that we use either the
|
||||
// // default iOS ringtone OR the custom ringtone associated with this user's
|
||||
// // system contact, if possible (iOS 11 or later).
|
||||
//
|
||||
// if #available(iOS 11.0, *) {
|
||||
// providerConfiguration.includesCallsInRecents = useSystemCallLog
|
||||
// } else {
|
||||
// // not configurable for iOS10+
|
||||
// assert(useSystemCallLog)
|
||||
// }
|
||||
//
|
||||
// return providerConfiguration
|
||||
// }
|
||||
//
|
||||
// init(callService: CallService, contactsManager: OWSContactsManager, notificationPresenter: NotificationPresenter, showNamesOnCallScreen: Bool, useSystemCallLog: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.debug("")
|
||||
//
|
||||
// self.callManager = CallKitCallManager(showNamesOnCallScreen: showNamesOnCallScreen)
|
||||
// self.callService = callService
|
||||
// self.contactsManager = contactsManager
|
||||
// self.notificationPresenter = notificationPresenter
|
||||
//
|
||||
// self.provider = type(of: self).sharedProvider(useSystemCallLog: useSystemCallLog)
|
||||
//
|
||||
// self.audioActivity = AudioActivity(audioDescription: "[CallKitCallUIAdaptee]", behavior: .call)
|
||||
// self.showNamesOnCallScreen = showNamesOnCallScreen
|
||||
//
|
||||
// super.init()
|
||||
//
|
||||
// // We cannot assert singleton here, because this class gets rebuilt when the user changes relevant call settings
|
||||
//
|
||||
// self.provider.setDelegate(self, queue: nil)
|
||||
// }
|
||||
//
|
||||
// // MARK: Dependencies
|
||||
//
|
||||
// var audioSession: OWSAudioSession {
|
||||
// return Environment.shared.audioSession
|
||||
// }
|
||||
//
|
||||
// // MARK: CallUIAdaptee
|
||||
//
|
||||
// func startOutgoingCall(handle: String) -> SignalCall {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// let call = SignalCall.outgoingCall(localId: UUID(), remotePhoneNumber: handle)
|
||||
//
|
||||
// // make sure we don't terminate audio session during call
|
||||
// _ = self.audioSession.startAudioActivity(call.audioActivity)
|
||||
//
|
||||
// // Add the new outgoing call to the app's list of calls.
|
||||
// // So we can find it in the provider delegate callbacks.
|
||||
// callManager.addCall(call)
|
||||
// callManager.startCall(call)
|
||||
//
|
||||
// return call
|
||||
// }
|
||||
//
|
||||
// // Called from CallService after call has ended to clean up any remaining CallKit call state.
|
||||
// func failCall(_ call: SignalCall, error: CallError) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// switch error {
|
||||
// case .timeout(description: _):
|
||||
// provider.reportCall(with: call.localId, endedAt: Date(), reason: CXCallEndedReason.unanswered)
|
||||
// default:
|
||||
// provider.reportCall(with: call.localId, endedAt: Date(), reason: CXCallEndedReason.failed)
|
||||
// }
|
||||
//
|
||||
// self.callManager.removeCall(call)
|
||||
// }
|
||||
//
|
||||
// func reportIncomingCall(_ call: SignalCall, callerName: String) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// // Construct a CXCallUpdate describing the incoming call, including the caller.
|
||||
// let update = CXCallUpdate()
|
||||
//
|
||||
// if showNamesOnCallScreen {
|
||||
// update.localizedCallerName = self.contactsManager.stringForConversationTitle(withPhoneIdentifier: call.remotePhoneNumber)
|
||||
// update.remoteHandle = CXHandle(type: .phoneNumber, value: call.remotePhoneNumber)
|
||||
// } else {
|
||||
// let callKitId = CallKitCallManager.kAnonymousCallHandlePrefix + call.localId.uuidString
|
||||
// update.remoteHandle = CXHandle(type: .generic, value: callKitId)
|
||||
// OWSPrimaryStorage.shared().setPhoneNumber(call.remotePhoneNumber, forCallKitId: callKitId)
|
||||
// update.localizedCallerName = NSLocalizedString("CALLKIT_ANONYMOUS_CONTACT_NAME", comment: "The generic name used for calls if CallKit privacy is enabled")
|
||||
// }
|
||||
//
|
||||
// update.hasVideo = call.hasLocalVideo
|
||||
//
|
||||
// disableUnsupportedFeatures(callUpdate: update)
|
||||
//
|
||||
// // Report the incoming call to the system
|
||||
// provider.reportNewIncomingCall(with: call.localId, update: update) { error in
|
||||
// /*
|
||||
// Only add incoming call to the app's list of calls if the call was allowed (i.e. there was no error)
|
||||
// since calls may be "denied" for various legitimate reasons. See CXErrorCodeIncomingCallError.
|
||||
// */
|
||||
// guard error == nil else {
|
||||
// Logger.error("failed to report new incoming call")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.callManager.addCall(call)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func answerCall(localId: UUID) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// owsFailDebug("CallKit should answer calls via system call screen, not via notifications.")
|
||||
// }
|
||||
//
|
||||
// func answerCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// callManager.answer(call: call)
|
||||
// }
|
||||
//
|
||||
// func declineCall(localId: UUID) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// owsFailDebug("CallKit should decline calls via system call screen, not via notifications.")
|
||||
// }
|
||||
//
|
||||
// func declineCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// callManager.localHangup(call: call)
|
||||
// }
|
||||
//
|
||||
// func recipientAcceptedCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// self.provider.reportOutgoingCall(with: call.localId, connectedAt: nil)
|
||||
//
|
||||
// let update = CXCallUpdate()
|
||||
// disableUnsupportedFeatures(callUpdate: update)
|
||||
//
|
||||
// provider.reportCall(with: call.localId, updated: update)
|
||||
// }
|
||||
//
|
||||
// func localHangupCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// callManager.localHangup(call: call)
|
||||
// }
|
||||
//
|
||||
// func remoteDidHangupCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// provider.reportCall(with: call.localId, endedAt: nil, reason: CXCallEndedReason.remoteEnded)
|
||||
// }
|
||||
//
|
||||
// func remoteBusy(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// provider.reportCall(with: call.localId, endedAt: nil, reason: CXCallEndedReason.unanswered)
|
||||
// }
|
||||
//
|
||||
// func setIsMuted(call: SignalCall, isMuted: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// callManager.setIsMuted(call: call, isMuted: isMuted)
|
||||
// }
|
||||
//
|
||||
// func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// let update = CXCallUpdate()
|
||||
// update.hasVideo = hasLocalVideo
|
||||
//
|
||||
// // Update the CallKit UI.
|
||||
// provider.reportCall(with: call.localId, updated: update)
|
||||
//
|
||||
// self.callService.setHasLocalVideo(hasLocalVideo: hasLocalVideo)
|
||||
// }
|
||||
//
|
||||
// // MARK: CXProviderDelegate
|
||||
//
|
||||
// func providerDidReset(_ provider: CXProvider) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// // End any ongoing calls if the provider resets, and remove them from the app's list of calls,
|
||||
// // since they are no longer valid.
|
||||
// callService.handleFailedCurrentCall(error: .providerReset)
|
||||
//
|
||||
// // Remove all calls from the app's list of calls.
|
||||
// callManager.removeAllCalls()
|
||||
// }
|
||||
//
|
||||
// func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.info("CXStartCallAction")
|
||||
//
|
||||
// guard let call = callManager.callWithLocalId(action.callUUID) else {
|
||||
// Logger.error("unable to find call")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // We can't wait for long before fulfilling the CXAction, else CallKit will show a "Failed Call". We don't
|
||||
// // actually need to wait for the outcome of the handleOutgoingCall promise, because it handles any errors by
|
||||
// // manually failing the call.
|
||||
// self.callService.handleOutgoingCall(call).retainUntilComplete()
|
||||
//
|
||||
// action.fulfill()
|
||||
// self.provider.reportOutgoingCall(with: call.localId, startedConnectingAt: nil)
|
||||
//
|
||||
// // Update the name used in the CallKit UI for outgoing calls when the user prefers not to show names
|
||||
// // in ther notifications
|
||||
// if !showNamesOnCallScreen {
|
||||
// let update = CXCallUpdate()
|
||||
// update.localizedCallerName = NSLocalizedString("CALLKIT_ANONYMOUS_CONTACT_NAME",
|
||||
// comment: "The generic name used for calls if CallKit privacy is enabled")
|
||||
// provider.reportCall(with: call.localId, updated: update)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.info("Received \(#function) CXAnswerCallAction")
|
||||
// // Retrieve the instance corresponding to the action's call UUID
|
||||
// guard let call = callManager.callWithLocalId(action.callUUID) else {
|
||||
// action.fail()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.callService.handleAnswerCall(call)
|
||||
// self.showCall(call)
|
||||
// action.fulfill()
|
||||
// }
|
||||
//
|
||||
// public func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.info("Received \(#function) CXEndCallAction")
|
||||
// guard let call = callManager.callWithLocalId(action.callUUID) else {
|
||||
// Logger.error("trying to end unknown call with localId: \(action.callUUID)")
|
||||
// action.fail()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.callService.handleLocalHungupCall(call)
|
||||
//
|
||||
// // Signal to the system that the action has been successfully performed.
|
||||
// action.fulfill()
|
||||
//
|
||||
// // Remove the ended call from the app's list of calls.
|
||||
// self.callManager.removeCall(call)
|
||||
// }
|
||||
//
|
||||
// public func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.info("Received \(#function) CXSetHeldCallAction")
|
||||
// guard let call = callManager.callWithLocalId(action.callUUID) else {
|
||||
// action.fail()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Update the SignalCall's underlying hold state.
|
||||
// self.callService.setIsOnHold(call: call, isOnHold: action.isOnHold)
|
||||
//
|
||||
// // Signal to the system that the action has been successfully performed.
|
||||
// action.fulfill()
|
||||
// }
|
||||
//
|
||||
// public func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.info("Received \(#function) CXSetMutedCallAction")
|
||||
// guard let call = callManager.callWithLocalId(action.callUUID) else {
|
||||
// Logger.error("Failing CXSetMutedCallAction for unknown call: \(action.callUUID)")
|
||||
// action.fail()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.callService.setIsMuted(call: call, isMuted: action.isMuted)
|
||||
// action.fulfill()
|
||||
// }
|
||||
//
|
||||
// public func provider(_ provider: CXProvider, perform action: CXSetGroupCallAction) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.warn("unimplemented \(#function) for CXSetGroupCallAction")
|
||||
// }
|
||||
//
|
||||
// public func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.warn("unimplemented \(#function) for CXPlayDTMFCallAction")
|
||||
// }
|
||||
//
|
||||
// func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// owsFailDebug("Timed out while performing \(action)")
|
||||
//
|
||||
// // React to the action timeout if necessary, such as showing an error UI.
|
||||
// }
|
||||
//
|
||||
// func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.debug("Received")
|
||||
//
|
||||
// _ = self.audioSession.startAudioActivity(self.audioActivity)
|
||||
// self.audioSession.isRTCAudioEnabled = true
|
||||
// }
|
||||
//
|
||||
// func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.debug("Received")
|
||||
// self.audioSession.isRTCAudioEnabled = false
|
||||
// self.audioSession.endAudioActivity(self.audioActivity)
|
||||
// }
|
||||
//
|
||||
// // MARK: - Util
|
||||
//
|
||||
// private func disableUnsupportedFeatures(callUpdate: CXCallUpdate) {
|
||||
// // Call Holding is failing to restart audio when "swapping" calls on the CallKit screen
|
||||
// // until user returns to in-app call screen.
|
||||
// callUpdate.supportsHolding = false
|
||||
//
|
||||
// // Not yet supported
|
||||
// callUpdate.supportsGrouping = false
|
||||
// callUpdate.supportsUngrouping = false
|
||||
//
|
||||
// // Is there any reason to support this?
|
||||
// callUpdate.supportsDTMF = false
|
||||
// }
|
||||
//}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,307 +0,0 @@
|
|||
////
|
||||
//// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
////
|
||||
//
|
||||
//import Foundation
|
||||
//import PromiseKit
|
||||
//import CallKit
|
||||
//import SignalUtilitiesKit
|
||||
//import SignalUtilitiesKit
|
||||
//import WebRTC
|
||||
//
|
||||
//protocol CallUIAdaptee {
|
||||
// var notificationPresenter: NotificationPresenter { get }
|
||||
// var callService: CallService { get }
|
||||
// var hasManualRinger: Bool { get }
|
||||
//
|
||||
// func startOutgoingCall(handle: String) -> SignalCall
|
||||
// func reportIncomingCall(_ call: SignalCall, callerName: String)
|
||||
// func reportMissedCall(_ call: SignalCall, callerName: String)
|
||||
// func answerCall(localId: UUID)
|
||||
// func answerCall(_ call: SignalCall)
|
||||
// func declineCall(localId: UUID)
|
||||
// func declineCall(_ call: SignalCall)
|
||||
// func recipientAcceptedCall(_ call: SignalCall)
|
||||
// func localHangupCall(_ call: SignalCall)
|
||||
// func remoteDidHangupCall(_ call: SignalCall)
|
||||
// func remoteBusy(_ call: SignalCall)
|
||||
// func failCall(_ call: SignalCall, error: CallError)
|
||||
// func setIsMuted(call: SignalCall, isMuted: Bool)
|
||||
// func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool)
|
||||
// func startAndShowOutgoingCall(recipientId: String, hasLocalVideo: Bool)
|
||||
//}
|
||||
//
|
||||
//// Shared default implementations
|
||||
//extension CallUIAdaptee {
|
||||
// internal func showCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// let callViewController = CallViewController(call: call)
|
||||
// callViewController.modalTransitionStyle = .crossDissolve
|
||||
//
|
||||
// if CallViewController.kShowCallViewOnSeparateWindow {
|
||||
// OWSWindowManager.shared().startCall(callViewController)
|
||||
// } else {
|
||||
// guard let presentingViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts else {
|
||||
// owsFailDebug("view controller unexpectedly nil")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if let presentedViewController = presentingViewController.presentedViewController {
|
||||
// presentedViewController.dismiss(animated: false) {
|
||||
// presentingViewController.present(callViewController, animated: true)
|
||||
// }
|
||||
// } else {
|
||||
// presentingViewController.present(callViewController, animated: true)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// internal func reportMissedCall(_ call: SignalCall, callerName: String) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// notificationPresenter.presentMissedCall(call, callerName: callerName)
|
||||
// }
|
||||
//
|
||||
// internal func startAndShowOutgoingCall(recipientId: String, hasLocalVideo: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// guard self.callService.call == nil else {
|
||||
// owsFailDebug("unexpectedly found an existing call when trying to start outgoing call: \(recipientId)")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let call = self.startOutgoingCall(handle: recipientId)
|
||||
// call.hasLocalVideo = hasLocalVideo
|
||||
// self.showCall(call)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
///**
|
||||
// * Notify the user of call related activities.
|
||||
// * Driven by either a CallKit or System notifications adaptee
|
||||
// */
|
||||
//@objc public class CallUIAdapter: NSObject, CallServiceObserver {
|
||||
//
|
||||
// private let adaptee: CallUIAdaptee
|
||||
// private let contactsManager: OWSContactsManager
|
||||
// internal let audioService: CallAudioService
|
||||
// internal let callService: CallService
|
||||
//
|
||||
// public required init(callService: CallService, contactsManager: OWSContactsManager, notificationPresenter: NotificationPresenter) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// self.contactsManager = contactsManager
|
||||
// self.callService = callService
|
||||
//
|
||||
// if Platform.isSimulator {
|
||||
// // CallKit doesn't seem entirely supported in simulator.
|
||||
// // e.g. you can't receive calls in the call screen.
|
||||
// // So we use the non-CallKit call UI.
|
||||
// Logger.info("choosing non-callkit adaptee for simulator.")
|
||||
// adaptee = NonCallKitCallUIAdaptee(callService: callService, notificationPresenter: notificationPresenter)
|
||||
// } else if CallUIAdapter.isCallkitDisabledForLocale {
|
||||
// Logger.info("choosing non-callkit adaptee due to locale.")
|
||||
// adaptee = NonCallKitCallUIAdaptee(callService: callService, notificationPresenter: notificationPresenter)
|
||||
// } else if #available(iOS 11, *) {
|
||||
// Logger.info("choosing callkit adaptee for iOS11+")
|
||||
// let showNames = Environment.shared.preferences.notificationPreviewType() != .noNameNoPreview
|
||||
// let useSystemCallLog = Environment.shared.preferences.isSystemCallLogEnabled()
|
||||
//
|
||||
// adaptee = CallKitCallUIAdaptee(callService: callService, contactsManager: contactsManager, notificationPresenter: notificationPresenter, showNamesOnCallScreen: showNames, useSystemCallLog: useSystemCallLog)
|
||||
// } else if #available(iOS 10.0, *), Environment.shared.preferences.isCallKitEnabled() {
|
||||
// Logger.info("choosing callkit adaptee for iOS10")
|
||||
// let hideNames = Environment.shared.preferences.isCallKitPrivacyEnabled() || Environment.shared.preferences.notificationPreviewType() == .noNameNoPreview
|
||||
// let showNames = !hideNames
|
||||
//
|
||||
// // All CallKit calls use the system call log on iOS10
|
||||
// let useSystemCallLog = true
|
||||
//
|
||||
// adaptee = CallKitCallUIAdaptee(callService: callService, contactsManager: contactsManager, notificationPresenter: notificationPresenter, showNamesOnCallScreen: showNames, useSystemCallLog: useSystemCallLog)
|
||||
// } else {
|
||||
// Logger.info("choosing non-callkit adaptee")
|
||||
// adaptee = NonCallKitCallUIAdaptee(callService: callService, notificationPresenter: notificationPresenter)
|
||||
// }
|
||||
//
|
||||
// audioService = CallAudioService(handleRinging: adaptee.hasManualRinger)
|
||||
//
|
||||
// super.init()
|
||||
//
|
||||
// // We cannot assert singleton here, because this class gets rebuilt when the user changes relevant call settings
|
||||
//
|
||||
// callService.addObserverAndSyncState(observer: self)
|
||||
// }
|
||||
//
|
||||
// @objc
|
||||
// public static var isCallkitDisabledForLocale: Bool {
|
||||
// let locale = Locale.current
|
||||
// guard let regionCode = locale.regionCode else {
|
||||
// owsFailDebug("Missing region code.")
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// // Apple has stopped approving apps that use CallKit functionality in mainland China.
|
||||
// // When the "CN" region is enabled, this check simply switches to the same pre-CallKit
|
||||
// // interface that is still used by everyone on iOS 9.
|
||||
// //
|
||||
// // For further reference: https://forums.developer.apple.com/thread/103083
|
||||
// return regionCode == "CN"
|
||||
// }
|
||||
//
|
||||
// // MARK: Dependencies
|
||||
//
|
||||
// var audioSession: OWSAudioSession {
|
||||
// return Environment.shared.audioSession
|
||||
// }
|
||||
//
|
||||
// // MARK:
|
||||
//
|
||||
// internal func reportIncomingCall(_ call: SignalCall, thread: TSContactThread) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// // make sure we don't terminate audio session during call
|
||||
// _ = audioSession.startAudioActivity(call.audioActivity)
|
||||
//
|
||||
// let callerName = self.contactsManager.displayName(forPhoneIdentifier: call.remotePhoneNumber)
|
||||
// adaptee.reportIncomingCall(call, callerName: callerName)
|
||||
// }
|
||||
//
|
||||
// internal func reportMissedCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// let callerName = self.contactsManager.displayName(forPhoneIdentifier: call.remotePhoneNumber)
|
||||
// adaptee.reportMissedCall(call, callerName: callerName)
|
||||
// }
|
||||
//
|
||||
// internal func startOutgoingCall(handle: String) -> SignalCall {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// let call = adaptee.startOutgoingCall(handle: handle)
|
||||
// return call
|
||||
// }
|
||||
//
|
||||
// @objc public func answerCall(localId: UUID) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.answerCall(localId: localId)
|
||||
// }
|
||||
//
|
||||
// internal func answerCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.answerCall(call)
|
||||
// }
|
||||
//
|
||||
// @objc public func declineCall(localId: UUID) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.declineCall(localId: localId)
|
||||
// }
|
||||
//
|
||||
// internal func declineCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.declineCall(call)
|
||||
// }
|
||||
//
|
||||
// internal func didTerminateCall(_ call: SignalCall?) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// if let call = call {
|
||||
// self.audioSession.endAudioActivity(call.audioActivity)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @objc public func startAndShowOutgoingCall(recipientId: String, hasLocalVideo: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.startAndShowOutgoingCall(recipientId: recipientId, hasLocalVideo: hasLocalVideo)
|
||||
// }
|
||||
//
|
||||
// internal func recipientAcceptedCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.recipientAcceptedCall(call)
|
||||
// }
|
||||
//
|
||||
// internal func remoteDidHangupCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.remoteDidHangupCall(call)
|
||||
// }
|
||||
//
|
||||
// internal func remoteBusy(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.remoteBusy(call)
|
||||
// }
|
||||
//
|
||||
// internal func localHangupCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.localHangupCall(call)
|
||||
// }
|
||||
//
|
||||
// internal func failCall(_ call: SignalCall, error: CallError) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.failCall(call, error: error)
|
||||
// }
|
||||
//
|
||||
// internal func showCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.showCall(call)
|
||||
// }
|
||||
//
|
||||
// internal func setIsMuted(call: SignalCall, isMuted: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// // With CallKit, muting is handled by a CXAction, so it must go through the adaptee
|
||||
// adaptee.setIsMuted(call: call, isMuted: isMuted)
|
||||
// }
|
||||
//
|
||||
// internal func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.setHasLocalVideo(call: call, hasLocalVideo: hasLocalVideo)
|
||||
// }
|
||||
//
|
||||
// internal func setAudioSource(call: SignalCall, audioSource: AudioSource?) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// // AudioSource is not handled by CallKit (e.g. there is no CXAction), so we handle it w/o going through the
|
||||
// // adaptee, relying on the AudioService CallObserver to put the system in a state consistent with the call's
|
||||
// // assigned property.
|
||||
// call.audioSource = audioSource
|
||||
// }
|
||||
//
|
||||
// internal func setCameraSource(call: SignalCall, isUsingFrontCamera: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// callService.setCameraSource(call: call, isUsingFrontCamera: isUsingFrontCamera)
|
||||
// }
|
||||
//
|
||||
// // CallKit handles ringing state on it's own. But for non-call kit we trigger ringing start/stop manually.
|
||||
// internal var hasManualRinger: Bool {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// return adaptee.hasManualRinger
|
||||
// }
|
||||
//
|
||||
// // MARK: - CallServiceObserver
|
||||
//
|
||||
// internal func didUpdateCall(call: SignalCall?) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// call?.addObserverAndSyncState(observer: audioService)
|
||||
// }
|
||||
//
|
||||
// internal func didUpdateVideoTracks(call: SignalCall?,
|
||||
// localCaptureSession: AVCaptureSession?,
|
||||
// remoteVideoTrack: RTCVideoTrack?) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// audioService.didUpdateVideoTracks(call: call)
|
||||
// }
|
||||
//}
|
|
@ -1,83 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol CallVideoHintViewDelegate: AnyObject {
|
||||
func didTapCallVideoHintView(_ videoHintView: CallVideoHintView)
|
||||
}
|
||||
|
||||
class CallVideoHintView: UIView {
|
||||
let label = UILabel()
|
||||
var tapGesture: UITapGestureRecognizer!
|
||||
weak var delegate: CallVideoHintViewDelegate?
|
||||
|
||||
let kTailHMargin: CGFloat = 12
|
||||
let kTailWidth: CGFloat = 16
|
||||
let kTailHeight: CGFloat = 8
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap(tapGesture:)))
|
||||
addGestureRecognizer(tapGesture)
|
||||
|
||||
let layerView = OWSLayerView(frame: .zero) { _ in }
|
||||
let shapeLayer = CAShapeLayer()
|
||||
shapeLayer.fillColor = UIColor.ows_signalBlue.cgColor
|
||||
layerView.layer.addSublayer(shapeLayer)
|
||||
addSubview(layerView)
|
||||
layerView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
let container = UIView()
|
||||
addSubview(container)
|
||||
container.autoSetDimension(.width, toSize: ScaleFromIPhone5(250), relation: .lessThanOrEqual)
|
||||
container.layoutMargins = UIEdgeInsets(top: 7, leading: 12, bottom: 7, trailing: 12)
|
||||
container.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: 0, leading: 0, bottom: kTailHeight, trailing: 0))
|
||||
|
||||
container.addSubview(label)
|
||||
label.autoPinEdgesToSuperviewMargins()
|
||||
label.setCompressionResistanceHigh()
|
||||
label.setContentHuggingHigh()
|
||||
label.font = UIFont.ows_dynamicTypeBody
|
||||
label.textColor = .ows_white
|
||||
label.numberOfLines = 0
|
||||
label.text = NSLocalizedString("CALL_VIEW_ENABLE_VIDEO_HINT", comment: "tooltip label when remote party has enabled their video")
|
||||
|
||||
layerView.layoutCallback = { view in
|
||||
let bezierPath = UIBezierPath()
|
||||
|
||||
// Bubble
|
||||
let bubbleBounds = container.bounds
|
||||
bezierPath.append(UIBezierPath(roundedRect: bubbleBounds, cornerRadius: 8))
|
||||
|
||||
// Tail
|
||||
var tailBottom = CGPoint(x: self.kTailHMargin + self.kTailWidth * 0.5, y: view.height())
|
||||
var tailLeft = CGPoint(x: self.kTailHMargin, y: view.height() - self.kTailHeight)
|
||||
var tailRight = CGPoint(x: self.kTailHMargin + self.kTailWidth, y: view.height() - self.kTailHeight)
|
||||
if (!CurrentAppContext().isRTL) {
|
||||
tailBottom.x = view.width() - tailBottom.x
|
||||
tailLeft.x = view.width() - tailLeft.x
|
||||
tailRight.x = view.width() - tailRight.x
|
||||
}
|
||||
bezierPath.move(to: tailBottom)
|
||||
bezierPath.addLine(to: tailLeft)
|
||||
bezierPath.addLine(to: tailRight)
|
||||
bezierPath.addLine(to: tailBottom)
|
||||
shapeLayer.path = bezierPath.cgPath
|
||||
shapeLayer.frame = view.bounds
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@objc
|
||||
func didTap(tapGesture: UITapGestureRecognizer) {
|
||||
self.delegate?.didTapCallVideoHintView(self)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,532 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc
|
||||
class OWSColorPickerAccessoryView: NeverClearView {
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return CGSize(width: kSwatchSize, height: kSwatchSize)
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
return self.intrinsicContentSize
|
||||
}
|
||||
|
||||
let kSwatchSize: CGFloat = 24
|
||||
|
||||
@objc
|
||||
required init(color: UIColor) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
let circleView = CircleView()
|
||||
circleView.backgroundColor = color
|
||||
addSubview(circleView)
|
||||
circleView.autoSetDimensions(to: CGSize(width: kSwatchSize, height: kSwatchSize))
|
||||
circleView.autoPinEdgesToSuperviewEdges()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@objc (OWSCircleView)
|
||||
class CircleView: UIView {
|
||||
override var bounds: CGRect {
|
||||
didSet {
|
||||
self.layer.cornerRadius = self.bounds.size.height / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol ColorViewDelegate: class {
|
||||
func colorViewWasTapped(_ colorView: ColorView)
|
||||
}
|
||||
|
||||
class ColorView: UIView {
|
||||
public weak var delegate: ColorViewDelegate?
|
||||
public let conversationColor: OWSConversationColor
|
||||
|
||||
private let swatchView: CircleView
|
||||
private let selectedRing: CircleView
|
||||
public var isSelected: Bool = false {
|
||||
didSet {
|
||||
self.selectedRing.isHidden = !isSelected
|
||||
}
|
||||
}
|
||||
|
||||
required init(conversationColor: OWSConversationColor) {
|
||||
self.conversationColor = conversationColor
|
||||
self.swatchView = CircleView()
|
||||
self.selectedRing = CircleView()
|
||||
|
||||
super.init(frame: .zero)
|
||||
self.addSubview(selectedRing)
|
||||
self.addSubview(swatchView)
|
||||
|
||||
// Selected Ring
|
||||
let cellHeight: CGFloat = ScaleFromIPhone5(60)
|
||||
selectedRing.autoSetDimensions(to: CGSize(width: cellHeight, height: cellHeight))
|
||||
|
||||
selectedRing.layer.borderColor = Theme.secondaryColor.cgColor
|
||||
selectedRing.layer.borderWidth = 2
|
||||
selectedRing.autoPinEdgesToSuperviewEdges()
|
||||
selectedRing.isHidden = true
|
||||
|
||||
// Color Swatch
|
||||
swatchView.backgroundColor = conversationColor.primaryColor
|
||||
|
||||
let swatchSize: CGFloat = ScaleFromIPhone5(46)
|
||||
swatchView.autoSetDimensions(to: CGSize(width: swatchSize, height: swatchSize))
|
||||
|
||||
swatchView.autoCenterInSuperview()
|
||||
|
||||
// gestures
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
|
||||
self.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@objc
|
||||
func didTap() {
|
||||
delegate?.colorViewWasTapped(self)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
protocol ColorPickerDelegate: class {
|
||||
func colorPicker(_ colorPicker: ColorPicker, didPickConversationColor conversationColor: OWSConversationColor)
|
||||
}
|
||||
|
||||
@objc(OWSColorPicker)
|
||||
class ColorPicker: NSObject, ColorPickerViewDelegate {
|
||||
|
||||
@objc
|
||||
public weak var delegate: ColorPickerDelegate?
|
||||
|
||||
@objc
|
||||
let sheetViewController: SheetViewController
|
||||
|
||||
@objc
|
||||
init(thread: TSThread) {
|
||||
let colorName = thread.conversationColorName
|
||||
let currentConversationColor = OWSConversationColor.conversationColorOrDefault(colorName: colorName)
|
||||
sheetViewController = SheetViewController()
|
||||
|
||||
super.init()
|
||||
|
||||
let colorPickerView = ColorPickerView(thread: thread)
|
||||
colorPickerView.delegate = self
|
||||
colorPickerView.select(conversationColor: currentConversationColor)
|
||||
|
||||
sheetViewController.contentView.addSubview(colorPickerView)
|
||||
colorPickerView.autoPinEdgesToSuperviewEdges()
|
||||
}
|
||||
|
||||
// MARK: ColorPickerViewDelegate
|
||||
|
||||
func colorPickerView(_ colorPickerView: ColorPickerView, didPickConversationColor conversationColor: OWSConversationColor) {
|
||||
self.delegate?.colorPicker(self, didPickConversationColor: conversationColor)
|
||||
}
|
||||
}
|
||||
|
||||
protocol ColorPickerViewDelegate: class {
|
||||
func colorPickerView(_ colorPickerView: ColorPickerView, didPickConversationColor conversationColor: OWSConversationColor)
|
||||
}
|
||||
|
||||
class ColorPickerView: UIView, ColorViewDelegate {
|
||||
|
||||
private let colorViews: [ColorView]
|
||||
let conversationStyle: ConversationStyle
|
||||
var outgoingMessageView = OWSMessageBubbleView(forAutoLayout: ())
|
||||
var incomingMessageView = OWSMessageBubbleView(forAutoLayout: ())
|
||||
weak var delegate: ColorPickerViewDelegate?
|
||||
|
||||
// This is mostly a developer convenience - OWSMessageCell asserts at some point
|
||||
// that the available method width is greater than 0.
|
||||
// We ultimately use the width of the picker view which will be larger.
|
||||
let kMinimumConversationWidth: CGFloat = 300
|
||||
override var bounds: CGRect {
|
||||
didSet {
|
||||
updateMockConversationView()
|
||||
}
|
||||
}
|
||||
|
||||
let mockConversationView: UIView = UIView()
|
||||
|
||||
init(thread: TSThread) {
|
||||
let allConversationColors = OWSConversationColor.conversationColorNames.map { OWSConversationColor.conversationColorOrDefault(colorName: $0) }
|
||||
|
||||
self.colorViews = allConversationColors.map { ColorView(conversationColor: $0) }
|
||||
|
||||
self.conversationStyle = ConversationStyle(thread: thread)
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
colorViews.forEach { $0.delegate = self }
|
||||
|
||||
let headerView = self.buildHeaderView()
|
||||
mockConversationView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
|
||||
mockConversationView.backgroundColor = Theme.backgroundColor
|
||||
self.updateMockConversationView()
|
||||
|
||||
let paletteView = self.buildPaletteView(colorViews: colorViews)
|
||||
|
||||
let rowsStackView = UIStackView(arrangedSubviews: [headerView, mockConversationView, paletteView])
|
||||
rowsStackView.axis = .vertical
|
||||
addSubview(rowsStackView)
|
||||
rowsStackView.autoPinEdgesToSuperviewEdges()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
// MARK: ColorViewDelegate
|
||||
|
||||
func colorViewWasTapped(_ colorView: ColorView) {
|
||||
self.select(conversationColor: colorView.conversationColor)
|
||||
self.delegate?.colorPickerView(self, didPickConversationColor: colorView.conversationColor)
|
||||
updateMockConversationView()
|
||||
}
|
||||
|
||||
fileprivate func select(conversationColor selectedConversationColor: OWSConversationColor) {
|
||||
colorViews.forEach { colorView in
|
||||
colorView.isSelected = colorView.conversationColor == selectedConversationColor
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: View Building
|
||||
|
||||
private func buildHeaderView() -> UIView {
|
||||
let headerView = UIView()
|
||||
headerView.layoutMargins = UIEdgeInsets(top: 15, left: 16, bottom: 15, right: 16)
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = NSLocalizedString("COLOR_PICKER_SHEET_TITLE", comment: "Modal Sheet title when picking a conversation color.")
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.font = UIFont.ows_dynamicTypeBody.ows_mediumWeight()
|
||||
titleLabel.textColor = Theme.primaryColor
|
||||
|
||||
headerView.addSubview(titleLabel)
|
||||
titleLabel.ows_autoPinToSuperviewMargins()
|
||||
|
||||
let bottomBorderView = UIView()
|
||||
bottomBorderView.backgroundColor = Theme.hairlineColor
|
||||
headerView.addSubview(bottomBorderView)
|
||||
bottomBorderView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top)
|
||||
bottomBorderView.autoSetDimension(.height, toSize: CGHairlineWidth())
|
||||
|
||||
return headerView
|
||||
}
|
||||
|
||||
private func updateMockConversationView() {
|
||||
/*
|
||||
conversationStyle.viewWidth = max(bounds.size.width, kMinimumConversationWidth)
|
||||
mockConversationView.subviews.forEach { $0.removeFromSuperview() }
|
||||
|
||||
// outgoing
|
||||
outgoingMessageView = OWSMessageBubbleView(forAutoLayout: ())
|
||||
let outgoingItem = MockConversationViewItem()
|
||||
let outgoingText = NSLocalizedString("COLOR_PICKER_DEMO_MESSAGE_1", comment: "The first of two messages demonstrating the chosen conversation color, by rendering this message in an outgoing message bubble.")
|
||||
outgoingItem.interaction = MockOutgoingMessage(messageBody: outgoingText)
|
||||
outgoingItem.displayableBodyText = DisplayableText.displayableText(outgoingText)
|
||||
outgoingItem.interactionType = .outgoingMessage
|
||||
|
||||
outgoingMessageView.viewItem = outgoingItem
|
||||
outgoingMessageView.cellMediaCache = NSCache()
|
||||
outgoingMessageView.conversationStyle = conversationStyle
|
||||
outgoingMessageView.configureViews()
|
||||
outgoingMessageView.loadContent()
|
||||
let outgoingCell = UIView()
|
||||
outgoingCell.addSubview(outgoingMessageView)
|
||||
outgoingMessageView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .leading)
|
||||
let outgoingSize = outgoingMessageView.measureSize()
|
||||
outgoingMessageView.autoSetDimensions(to: outgoingSize)
|
||||
|
||||
// incoming
|
||||
incomingMessageView = OWSMessageBubbleView(forAutoLayout: ())
|
||||
let incomingItem = MockConversationViewItem()
|
||||
let incomingText = NSLocalizedString("COLOR_PICKER_DEMO_MESSAGE_2", comment: "The second of two messages demonstrating the chosen conversation color, by rendering this message in an incoming message bubble.")
|
||||
incomingItem.interaction = MockIncomingMessage(messageBody: incomingText)
|
||||
incomingItem.displayableBodyText = DisplayableText.displayableText(incomingText)
|
||||
incomingItem.interactionType = .incomingMessage
|
||||
|
||||
incomingMessageView.viewItem = incomingItem
|
||||
incomingMessageView.cellMediaCache = NSCache()
|
||||
incomingMessageView.conversationStyle = conversationStyle
|
||||
incomingMessageView.configureViews()
|
||||
incomingMessageView.loadContent()
|
||||
let incomingCell = UIView()
|
||||
incomingCell.addSubview(incomingMessageView)
|
||||
incomingMessageView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .trailing)
|
||||
let incomingSize = incomingMessageView.measureSize()
|
||||
incomingMessageView.autoSetDimensions(to: incomingSize)
|
||||
|
||||
let messagesStackView = UIStackView(arrangedSubviews: [outgoingCell, incomingCell])
|
||||
messagesStackView.axis = .vertical
|
||||
messagesStackView.spacing = 12
|
||||
|
||||
mockConversationView.addSubview(messagesStackView)
|
||||
messagesStackView.autoPinEdgesToSuperviewMargins()
|
||||
*/
|
||||
}
|
||||
|
||||
private func buildPaletteView(colorViews: [ColorView]) -> UIView {
|
||||
let paletteView = UIView()
|
||||
let paletteMargin = ScaleFromIPhone5(12)
|
||||
paletteView.layoutMargins = UIEdgeInsets(top: paletteMargin, left: paletteMargin, bottom: 0, right: paletteMargin)
|
||||
|
||||
let kRowLength = 4
|
||||
let rows: [UIView] = colorViews.chunked(by: kRowLength).map { colorViewsInRow in
|
||||
let row = UIStackView(arrangedSubviews: colorViewsInRow)
|
||||
row.distribution = UIStackView.Distribution.equalSpacing
|
||||
return row
|
||||
}
|
||||
let rowsStackView = UIStackView(arrangedSubviews: rows)
|
||||
rowsStackView.axis = .vertical
|
||||
rowsStackView.spacing = ScaleFromIPhone5To7Plus(12, 30)
|
||||
|
||||
paletteView.addSubview(rowsStackView)
|
||||
rowsStackView.ows_autoPinToSuperviewMargins()
|
||||
|
||||
// no-op gesture to keep taps from dismissing SheetView
|
||||
paletteView.addGestureRecognizer(UITapGestureRecognizer(target: nil, action: nil))
|
||||
return paletteView
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Mock Classes for rendering demo conversation
|
||||
|
||||
/*
|
||||
@objc
|
||||
private class MockConversationViewItem: NSObject, ConversationViewItem {
|
||||
var userCanDeleteGroupMessage: Bool = false
|
||||
var isRSSFeed: Bool = false
|
||||
var interaction: TSInteraction = TSMessage()
|
||||
var interactionType: OWSInteractionType = OWSInteractionType.unknown
|
||||
var quotedReply: OWSQuotedReplyModel?
|
||||
var isGroupThread: Bool = false
|
||||
var hasBodyText: Bool = true
|
||||
var isQuotedReply: Bool = false
|
||||
var hasQuotedAttachment: Bool = false
|
||||
var hasQuotedText: Bool = false
|
||||
var hasCellHeader: Bool = false
|
||||
var isExpiringMessage: Bool = false
|
||||
var shouldShowDate: Bool = false
|
||||
var shouldShowSenderAvatar: Bool = false
|
||||
var senderName: NSAttributedString?
|
||||
var shouldHideFooter: Bool = false
|
||||
var isFirstInCluster: Bool = true
|
||||
var isLastInCluster: Bool = true
|
||||
var unreadIndicator: OWSUnreadIndicator?
|
||||
var lastAudioMessageView: OWSAudioMessageView?
|
||||
var audioDurationSeconds: CGFloat = 0
|
||||
var audioProgressSeconds: CGFloat = 0
|
||||
var messageCellType: OWSMessageCellType = .textOnlyMessage
|
||||
var displayableBodyText: DisplayableText?
|
||||
var attachmentStream: TSAttachmentStream?
|
||||
var attachmentPointer: TSAttachmentPointer?
|
||||
var mediaSize: CGSize = .zero
|
||||
var displayableQuotedText: DisplayableText?
|
||||
var quotedAttachmentMimetype: String?
|
||||
var quotedRecipientId: String?
|
||||
var didCellMediaFailToLoad: Bool = false
|
||||
var contactShare: ContactShareViewModel?
|
||||
var systemMessageText: String?
|
||||
var authorConversationColorName: String?
|
||||
var hasBodyTextActionContent: Bool = false
|
||||
var hasMediaActionContent: Bool = false
|
||||
var mediaAlbumItems: [ConversationMediaAlbumItem]?
|
||||
var hasCachedLayoutState: Bool = false
|
||||
var linkPreview: OWSLinkPreview?
|
||||
var linkPreviewAttachment: TSAttachment?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
func itemId() -> String {
|
||||
return interaction.uniqueId!
|
||||
}
|
||||
|
||||
func dequeueCell(for collectionView: UICollectionView, indexPath: IndexPath) -> ConversationViewCell {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return ConversationViewCell(forAutoLayout: ())
|
||||
}
|
||||
|
||||
func replace(_ interaction: TSInteraction, transaction: YapDatabaseReadTransaction) {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func clearCachedLayoutState() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func copyMediaAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func copyTextAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func shareMediaAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func shareTextAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func saveMediaAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func deleteAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func canCopyMedia() -> Bool {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return false
|
||||
}
|
||||
|
||||
func canSaveMedia() -> Bool {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return false
|
||||
}
|
||||
|
||||
func audioPlaybackState() -> AudioPlaybackState {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return AudioPlaybackState.paused
|
||||
}
|
||||
|
||||
func setAudioPlaybackState(_ state: AudioPlaybackState) {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func setAudioProgress(_ progress: CGFloat, duration: CGFloat) {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func cellSize() -> CGSize {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return CGSize.zero
|
||||
}
|
||||
|
||||
func vSpacing(withPreviousLayoutItem previousLayoutItem: ConversationViewLayoutItem) -> CGFloat {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return 2
|
||||
}
|
||||
|
||||
func firstValidAlbumAttachment() -> TSAttachmentStream? {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return nil
|
||||
}
|
||||
|
||||
func mediaAlbumHasFailedAttachment() -> Bool {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return false
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
private class MockIncomingMessage: TSIncomingMessage {
|
||||
init(messageBody: String) {
|
||||
super.init(incomingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(),
|
||||
in: TSThread(),
|
||||
authorId: "+fake-id",
|
||||
sourceDeviceId: 1,
|
||||
messageBody: messageBody,
|
||||
attachmentIds: [],
|
||||
expiresInSeconds: 0,
|
||||
quotedMessage: nil,
|
||||
contactShare: nil,
|
||||
linkPreview: nil,
|
||||
serverTimestamp: nil,
|
||||
wasReceivedByUD: false)
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
required init(dictionary dictionaryValue: [String: Any]!) throws {
|
||||
fatalError("init(dictionary:) has not been implemented")
|
||||
}
|
||||
|
||||
override func save(with transaction: YapDatabaseReadWriteTransaction) {
|
||||
// no - op
|
||||
owsFailDebug("shouldn't save mock message")
|
||||
}
|
||||
}
|
||||
|
||||
private class MockOutgoingMessage: TSOutgoingMessage {
|
||||
init(messageBody: String) {
|
||||
super.init(outgoingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(),
|
||||
in: nil,
|
||||
messageBody: messageBody,
|
||||
attachmentIds: [],
|
||||
expiresInSeconds: 0,
|
||||
expireStartedAt: 0,
|
||||
isVoiceMessage: false,
|
||||
groupMetaMessage: .unspecified,
|
||||
quotedMessage: nil,
|
||||
contactShare: nil,
|
||||
linkPreview: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
required init(dictionary dictionaryValue: [String: Any]!) throws {
|
||||
fatalError("init(dictionary:) has not been implemented")
|
||||
}
|
||||
|
||||
override func save(with transaction: YapDatabaseReadWriteTransaction) {
|
||||
// no - op
|
||||
owsFailDebug("shouldn't save mock message")
|
||||
}
|
||||
|
||||
class MockOutgoingMessageRecipientState: TSOutgoingMessageRecipientState {
|
||||
override var state: OWSOutgoingMessageRecipientState {
|
||||
return OWSOutgoingMessageRecipientState.sent
|
||||
}
|
||||
|
||||
override var deliveryTimestamp: NSNumber? {
|
||||
return NSNumber(value: NSDate.ows_millisecondTimeStamp())
|
||||
}
|
||||
|
||||
override var readTimestamp: NSNumber? {
|
||||
return NSNumber(value: NSDate.ows_millisecondTimeStamp())
|
||||
}
|
||||
}
|
||||
|
||||
override func readRecipientIds() -> [String] {
|
||||
// makes message appear as read
|
||||
return ["fake-non-empty-id"]
|
||||
}
|
||||
|
||||
override func recipientState(forRecipientId recipientId: String) -> TSOutgoingMessageRecipientState? {
|
||||
return MockOutgoingMessageRecipientState()
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalUtilitiesKit
|
||||
|
||||
let CompareSafetyNumbersActivityType = "org.whispersystems.signal.activity.CompareSafetyNumbers"
|
||||
|
||||
@objc(OWSCompareSafetyNumbersActivityDelegate)
|
||||
protocol CompareSafetyNumbersActivityDelegate {
|
||||
func compareSafetyNumbersActivitySucceeded(activity: CompareSafetyNumbersActivity)
|
||||
func compareSafetyNumbersActivity(_ activity: CompareSafetyNumbersActivity, failedWithError error: Error)
|
||||
}
|
||||
|
||||
@objc (OWSCompareSafetyNumbersActivity)
|
||||
class CompareSafetyNumbersActivity: UIActivity {
|
||||
|
||||
var mySafetyNumbers: String?
|
||||
let delegate: CompareSafetyNumbersActivityDelegate
|
||||
|
||||
@objc
|
||||
required init(delegate: CompareSafetyNumbersActivityDelegate) {
|
||||
self.delegate = delegate
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: UIActivity
|
||||
|
||||
override class var activityCategory: UIActivity.Category {
|
||||
get { return .action }
|
||||
}
|
||||
|
||||
override var activityType: UIActivity.ActivityType? {
|
||||
get { return UIActivity.ActivityType(rawValue: CompareSafetyNumbersActivityType) }
|
||||
}
|
||||
|
||||
override var activityTitle: String? {
|
||||
get {
|
||||
return NSLocalizedString("COMPARE_SAFETY_NUMBER_ACTION", comment: "Activity Sheet label")
|
||||
}
|
||||
}
|
||||
|
||||
override var activityImage: UIImage? {
|
||||
get {
|
||||
return #imageLiteral(resourceName: "ic_lock_outline")
|
||||
}
|
||||
}
|
||||
|
||||
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
|
||||
return stringsFrom(activityItems: activityItems).count > 0
|
||||
}
|
||||
|
||||
override func prepare(withActivityItems activityItems: [Any]) {
|
||||
let myFormattedSafetyNumbers = stringsFrom(activityItems: activityItems).first
|
||||
mySafetyNumbers = numericOnly(string: myFormattedSafetyNumbers)
|
||||
}
|
||||
|
||||
override func perform() {
|
||||
defer { activityDidFinish(true) }
|
||||
|
||||
let pasteboardString = numericOnly(string: UIPasteboard.general.string)
|
||||
guard (pasteboardString != nil && pasteboardString!.count == 60) else {
|
||||
Logger.warn("no valid safety numbers found in pasteboard: \(String(describing: pasteboardString))")
|
||||
let error = OWSErrorWithCodeDescription(OWSErrorCode.userError,
|
||||
NSLocalizedString("PRIVACY_VERIFICATION_FAILED_NO_SAFETY_NUMBERS_IN_CLIPBOARD", comment: "Alert body for user error"))
|
||||
|
||||
delegate.compareSafetyNumbersActivity(self, failedWithError: error)
|
||||
return
|
||||
}
|
||||
|
||||
let pasteboardSafetyNumbers = pasteboardString!
|
||||
|
||||
if pasteboardSafetyNumbers == mySafetyNumbers {
|
||||
Logger.info("successfully matched safety numbers. local numbers: \(String(describing: mySafetyNumbers)) pasteboard:\(pasteboardSafetyNumbers)")
|
||||
delegate.compareSafetyNumbersActivitySucceeded(activity: self)
|
||||
} else {
|
||||
Logger.warn("local numbers: \(String(describing: mySafetyNumbers)) didn't match pasteboard:\(pasteboardSafetyNumbers)")
|
||||
let error = OWSErrorWithCodeDescription(OWSErrorCode.privacyVerificationFailure,
|
||||
NSLocalizedString("PRIVACY_VERIFICATION_FAILED_MISMATCHED_SAFETY_NUMBERS_IN_CLIPBOARD", comment: "Alert body"))
|
||||
delegate.compareSafetyNumbersActivity(self, failedWithError: error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
func numericOnly(string: String?) -> String? {
|
||||
guard let string = string else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var numericOnly: String?
|
||||
if let regex = try? NSRegularExpression(pattern: "\\D", options: .caseInsensitive) {
|
||||
numericOnly = regex.stringByReplacingMatches(in: string, options: .withTransparentBounds, range: NSRange(location: 0, length: string.utf16.count), withTemplate: "")
|
||||
}
|
||||
|
||||
return numericOnly
|
||||
}
|
||||
|
||||
func stringsFrom(activityItems: [Any]) -> [String] {
|
||||
return activityItems.map { $0 as? String }.filter { $0 != nil }.map { $0! }
|
||||
}
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Contacts
|
||||
import SignalUtilitiesKit
|
||||
|
||||
class ContactCell: UITableViewCell {
|
||||
|
||||
public static let kSeparatorHInset: CGFloat = CGFloat(kAvatarDiameter) + 16 + 8
|
||||
|
||||
static let kAvatarSpacing: CGFloat = 6
|
||||
static let kAvatarDiameter: UInt = 40
|
||||
|
||||
let contactImageView: AvatarImageView
|
||||
let textStackView: UIStackView
|
||||
let titleLabel: UILabel
|
||||
var subtitleLabel: UILabel
|
||||
|
||||
var contact: Contact?
|
||||
var showsWhenSelected: Bool = false
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
self.contactImageView = AvatarImageView()
|
||||
self.textStackView = UIStackView()
|
||||
self.titleLabel = UILabel()
|
||||
self.titleLabel.font = UIFont.ows_dynamicTypeBody
|
||||
self.subtitleLabel = UILabel()
|
||||
self.subtitleLabel.font = UIFont.ows_dynamicTypeSubheadline
|
||||
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
selectionStyle = UITableViewCell.SelectionStyle.none
|
||||
|
||||
textStackView.axis = .vertical
|
||||
textStackView.addArrangedSubview(titleLabel)
|
||||
|
||||
contactImageView.autoSetDimensions(to: CGSize(width: CGFloat(ContactCell.kAvatarDiameter), height: CGFloat(ContactCell.kAvatarDiameter)))
|
||||
|
||||
let contentColumns: UIStackView = UIStackView(arrangedSubviews: [contactImageView, textStackView])
|
||||
contentColumns.axis = .horizontal
|
||||
contentColumns.spacing = ContactCell.kAvatarSpacing
|
||||
contentColumns.alignment = .center
|
||||
|
||||
self.contentView.addSubview(contentColumns)
|
||||
contentColumns.autoPinEdgesToSuperviewMargins()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.didChangePreferredContentSize), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
accessoryType = .none
|
||||
self.subtitleLabel.removeFromSuperview()
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
if showsWhenSelected {
|
||||
accessoryType = selected ? .checkmark : .none
|
||||
}
|
||||
}
|
||||
|
||||
@objc func didChangePreferredContentSize() {
|
||||
self.titleLabel.font = UIFont.ows_dynamicTypeBody
|
||||
self.subtitleLabel.font = UIFont.ows_dynamicTypeSubheadline
|
||||
}
|
||||
|
||||
func configure(contact: Contact, subtitleType: SubtitleCellValue, showsWhenSelected: Bool, contactsManager: OWSContactsManager) {
|
||||
|
||||
OWSTableItem.configureCell(self)
|
||||
|
||||
self.contact = contact
|
||||
self.showsWhenSelected = showsWhenSelected
|
||||
|
||||
self.titleLabel.textColor = Theme.primaryColor
|
||||
self.subtitleLabel.textColor = Theme.secondaryColor
|
||||
|
||||
let cnContact = contactsManager.cnContact(withId: contact.cnContactId)
|
||||
titleLabel.attributedText = cnContact?.formattedFullName(font: titleLabel.font)
|
||||
updateSubtitle(subtitleType: subtitleType, contact: contact)
|
||||
|
||||
if let contactImage = contactsManager.avatarImage(forCNContactId: contact.cnContactId) {
|
||||
contactImageView.image = contactImage
|
||||
} else {
|
||||
let contactIdForDeterminingBackgroundColor: String
|
||||
if let signalId = contact.parsedPhoneNumbers.first?.toE164() {
|
||||
contactIdForDeterminingBackgroundColor = signalId
|
||||
} else {
|
||||
contactIdForDeterminingBackgroundColor = contact.fullName
|
||||
}
|
||||
|
||||
let avatarBuilder = OWSContactAvatarBuilder(nonSignalName: contact.fullName,
|
||||
colorSeed: contactIdForDeterminingBackgroundColor,
|
||||
diameter: ContactCell.kAvatarDiameter)
|
||||
|
||||
contactImageView.image = avatarBuilder.build()
|
||||
}
|
||||
}
|
||||
|
||||
func updateSubtitle(subtitleType: SubtitleCellValue, contact: Contact) {
|
||||
switch subtitleType {
|
||||
case .none:
|
||||
assert(self.subtitleLabel.superview == nil)
|
||||
break
|
||||
case .phoneNumber:
|
||||
self.textStackView.addArrangedSubview(self.subtitleLabel)
|
||||
|
||||
if let firstPhoneNumber = contact.userTextPhoneNumbers.first {
|
||||
self.subtitleLabel.text = firstPhoneNumber
|
||||
} else {
|
||||
self.subtitleLabel.text = NSLocalizedString("CONTACT_PICKER_NO_PHONE_NUMBERS_AVAILABLE", comment: "table cell subtitle when contact card has no known phone number")
|
||||
}
|
||||
case .email:
|
||||
self.textStackView.addArrangedSubview(self.subtitleLabel)
|
||||
|
||||
if let firstEmail = contact.emails.first {
|
||||
self.subtitleLabel.text = firstEmail
|
||||
} else {
|
||||
self.subtitleLabel.text = NSLocalizedString("CONTACT_PICKER_NO_EMAILS_AVAILABLE", comment: "table cell subtitle when contact card has no email")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension CNContact {
|
||||
/**
|
||||
* Bold the sorting portion of the name. e.g. if we sort by family name, bold the family name.
|
||||
*/
|
||||
func formattedFullName(font: UIFont) -> NSAttributedString? {
|
||||
let keyToHighlight = ContactSortOrder == .familyName ? CNContactFamilyNameKey : CNContactGivenNameKey
|
||||
|
||||
let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold)
|
||||
let boldAttributes = [
|
||||
NSAttributedString.Key.font: UIFont(descriptor: boldDescriptor!, size: 0)
|
||||
]
|
||||
|
||||
if let attributedName = CNContactFormatter.attributedString(from: self, style: .fullName, defaultAttributes: nil) {
|
||||
let highlightedName = attributedName.mutableCopy() as! NSMutableAttributedString
|
||||
highlightedName.enumerateAttributes(in: NSRange(location: 0, length: highlightedName.length), options: [], using: { (attrs, range, _) in
|
||||
if let property = attrs[NSAttributedString.Key(rawValue: CNContactPropertyAttribute)] as? String, property == keyToHighlight {
|
||||
highlightedName.addAttributes(boldAttributes, range: range)
|
||||
}
|
||||
})
|
||||
return highlightedName
|
||||
}
|
||||
|
||||
if let emailAddress = self.emailAddresses.first?.value {
|
||||
return NSAttributedString(string: emailAddress as String, attributes: boldAttributes)
|
||||
}
|
||||
|
||||
if let phoneNumber = self.phoneNumbers.first?.value.stringValue {
|
||||
return NSAttributedString(string: phoneNumber, attributes: boldAttributes)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -1,215 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalUtilitiesKit
|
||||
import ContactsUI
|
||||
import MessageUI
|
||||
|
||||
@objc
|
||||
public protocol ContactShareViewHelperDelegate: class {
|
||||
func didCreateOrEditContact()
|
||||
}
|
||||
|
||||
@objc
|
||||
public class ContactShareViewHelper: NSObject, CNContactViewControllerDelegate {
|
||||
|
||||
@objc
|
||||
weak var delegate: ContactShareViewHelperDelegate?
|
||||
|
||||
let contactsManager: OWSContactsManager
|
||||
|
||||
@objc
|
||||
public required init(contactsManager: OWSContactsManager) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
self.contactsManager = contactsManager
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@objc
|
||||
public func sendMessage(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
presentThreadAndPeform(action: .compose, contactShare: contactShare, fromViewController: fromViewController)
|
||||
}
|
||||
|
||||
@objc
|
||||
public func audioCall(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
presentThreadAndPeform(action: .audioCall, contactShare: contactShare, fromViewController: fromViewController)
|
||||
}
|
||||
|
||||
@objc
|
||||
public func videoCall(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
presentThreadAndPeform(action: .videoCall, contactShare: contactShare, fromViewController: fromViewController)
|
||||
}
|
||||
|
||||
private func presentThreadAndPeform(action: ConversationViewAction, contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
// TODO: We're taking the first Signal account id. We might
|
||||
// want to let the user select if there's more than one.
|
||||
let phoneNumbers = contactShare.systemContactsWithSignalAccountPhoneNumbers(contactsManager)
|
||||
guard phoneNumbers.count > 0 else {
|
||||
owsFailDebug("missing Signal recipient id.")
|
||||
return
|
||||
}
|
||||
guard phoneNumbers.count > 1 else {
|
||||
let recipientId = phoneNumbers.first!
|
||||
SignalApp.shared().presentConversation(forRecipientId: recipientId, action: action, animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
showPhoneNumberPicker(phoneNumbers: phoneNumbers, fromViewController: fromViewController, completion: { (recipientId) in
|
||||
SignalApp.shared().presentConversation(forRecipientId: recipientId, action: action, animated: true)
|
||||
})
|
||||
}
|
||||
|
||||
@objc
|
||||
public func showInviteContact(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
guard MFMessageComposeViewController.canSendText() else {
|
||||
Logger.info("Device cannot send text")
|
||||
OWSAlerts.showErrorAlert(message: NSLocalizedString("UNSUPPORTED_FEATURE_ERROR", comment: ""))
|
||||
return
|
||||
}
|
||||
let phoneNumbers = contactShare.e164PhoneNumbers()
|
||||
guard phoneNumbers.count > 0 else {
|
||||
owsFailDebug("no phone numbers.")
|
||||
return
|
||||
}
|
||||
|
||||
let inviteFlow = InviteFlow(presentingViewController: fromViewController)
|
||||
inviteFlow.sendSMSTo(phoneNumbers: phoneNumbers)
|
||||
}
|
||||
|
||||
@objc
|
||||
func showAddToContacts(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("CONVERSATION_SETTINGS_NEW_CONTACT",
|
||||
comment: "Label for 'new contact' button in conversation settings view."),
|
||||
style: .default) { _ in
|
||||
self.didPressCreateNewContact(contactShare: contactShare, fromViewController: fromViewController)
|
||||
})
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("CONVERSATION_SETTINGS_ADD_TO_EXISTING_CONTACT",
|
||||
comment: "Label for 'new contact' button in conversation settings view."),
|
||||
style: .default) { _ in
|
||||
self.didPressAddToExistingContact(contactShare: contactShare, fromViewController: fromViewController)
|
||||
})
|
||||
actionSheet.addAction(OWSAlerts.cancelAction)
|
||||
|
||||
fromViewController.presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
private func showPhoneNumberPicker(phoneNumbers: [String], fromViewController: UIViewController, completion :@escaping ((String) -> Void)) {
|
||||
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
for phoneNumber in phoneNumbers {
|
||||
actionSheet.addAction(UIAlertAction(title: PhoneNumber.bestEffortLocalizedPhoneNumber(withE164: phoneNumber),
|
||||
style: .default) { _ in
|
||||
completion(phoneNumber)
|
||||
})
|
||||
}
|
||||
actionSheet.addAction(OWSAlerts.cancelAction)
|
||||
|
||||
fromViewController.presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
func didPressCreateNewContact(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
presentNewContactView(contactShare: contactShare, fromViewController: fromViewController)
|
||||
}
|
||||
|
||||
func didPressAddToExistingContact(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
presentSelectAddToExistingContactView(contactShare: contactShare, fromViewController: fromViewController)
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private func presentNewContactView(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
guard contactsManager.supportsContactEditing else {
|
||||
owsFailDebug("Contact editing not supported")
|
||||
return
|
||||
}
|
||||
|
||||
guard let systemContact = OWSContacts.systemContact(for: contactShare.dbRecord, imageData: contactShare.avatarImageData) else {
|
||||
owsFailDebug("Could not derive system contact.")
|
||||
return
|
||||
}
|
||||
|
||||
guard contactsManager.isSystemContactsAuthorized else {
|
||||
ContactsViewHelper.presentMissingContactAccessAlertController(from: fromViewController)
|
||||
return
|
||||
}
|
||||
|
||||
let contactViewController = CNContactViewController(forNewContact: systemContact)
|
||||
contactViewController.delegate = self
|
||||
contactViewController.allowsActions = false
|
||||
contactViewController.allowsEditing = true
|
||||
contactViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: CommonStrings.cancelButton,
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(didFinishEditingContact))
|
||||
|
||||
let modal = OWSNavigationController(rootViewController: contactViewController)
|
||||
fromViewController.present(modal, animated: true)
|
||||
}
|
||||
|
||||
private func presentSelectAddToExistingContactView(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
guard contactsManager.supportsContactEditing else {
|
||||
owsFailDebug("Contact editing not supported")
|
||||
return
|
||||
}
|
||||
|
||||
guard contactsManager.isSystemContactsAuthorized else {
|
||||
ContactsViewHelper.presentMissingContactAccessAlertController(from: fromViewController)
|
||||
return
|
||||
}
|
||||
|
||||
guard let navigationController = fromViewController.navigationController else {
|
||||
owsFailDebug("missing navigationController")
|
||||
return
|
||||
}
|
||||
|
||||
let viewController = AddContactShareToExistingContactViewController(contactShare: contactShare)
|
||||
navigationController.pushViewController(viewController, animated: true)
|
||||
}
|
||||
|
||||
// MARK: - CNContactViewControllerDelegate
|
||||
|
||||
@objc public func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
|
||||
Logger.info("")
|
||||
|
||||
guard let delegate = delegate else {
|
||||
owsFailDebug("missing delegate")
|
||||
return
|
||||
}
|
||||
|
||||
delegate.didCreateOrEditContact()
|
||||
}
|
||||
|
||||
@objc public func didFinishEditingContact() {
|
||||
Logger.info("")
|
||||
|
||||
guard let delegate = delegate else {
|
||||
owsFailDebug("missing delegate")
|
||||
return
|
||||
}
|
||||
|
||||
delegate.didCreateOrEditContact()
|
||||
}
|
||||
}
|
|
@ -1,679 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalUtilitiesKit
|
||||
import SignalUtilitiesKit
|
||||
import Reachability
|
||||
import ContactsUI
|
||||
import MessageUI
|
||||
|
||||
class ContactViewController: OWSViewController, ContactShareViewHelperDelegate {
|
||||
|
||||
enum ContactViewMode {
|
||||
case systemContactWithSignal,
|
||||
systemContactWithoutSignal,
|
||||
nonSystemContact,
|
||||
noPhoneNumber,
|
||||
unknown
|
||||
}
|
||||
|
||||
private var hasLoadedView = false
|
||||
|
||||
private var viewMode = ContactViewMode.unknown {
|
||||
didSet {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
if oldValue != viewMode && hasLoadedView {
|
||||
updateContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let contactsManager: OWSContactsManager
|
||||
|
||||
private var reachability: Reachability?
|
||||
|
||||
private let contactShare: ContactShareViewModel
|
||||
|
||||
private var contactShareViewHelper: ContactShareViewHelper
|
||||
|
||||
private weak var postDismissNavigationController: UINavigationController?
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
@available(*, unavailable, message: "use init(call:) constructor instead.")
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
@objc
|
||||
required init(contactShare: ContactShareViewModel) {
|
||||
contactsManager = Environment.shared.contactsManager
|
||||
self.contactShare = contactShare
|
||||
self.contactShareViewHelper = ContactShareViewHelper(contactsManager: contactsManager)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
contactShareViewHelper.delegate = self
|
||||
|
||||
updateMode()
|
||||
|
||||
NotificationCenter.default.addObserver(forName: .OWSContactsManagerSignalAccountsDidChange, object: nil, queue: nil) { [weak self] _ in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.updateMode()
|
||||
}
|
||||
|
||||
reachability = Reachability.forInternetConnection()
|
||||
|
||||
NotificationCenter.default.addObserver(forName: .reachabilityChanged, object: nil, queue: nil) { [weak self] _ in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.updateMode()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - View Lifecycle
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
// self.navigationController is nil in viewWillDisappear when transition via message/call buttons
|
||||
// so we maintain our own reference to restore the navigation bars.
|
||||
postDismissNavigationController = navigationController
|
||||
navigationController.isNavigationBarHidden = true
|
||||
|
||||
contactsManager.requestSystemContactsOnce(completion: { [weak self] _ in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.updateMode()
|
||||
})
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
if self.presentedViewController == nil {
|
||||
// No need to do this when we're disappearing due to a modal presentation.
|
||||
// We'll eventually return to to this view and need to hide again. But also, there is a visible
|
||||
// animation glitch where the navigation bar for this view controller starts to appear while
|
||||
// the whole nav stack is about to be obscured by the modal we are presenting.
|
||||
guard let postDismissNavigationController = self.postDismissNavigationController else {
|
||||
owsFailDebug("postDismissNavigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
postDismissNavigationController.setNavigationBarHidden(false, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
super.loadView()
|
||||
|
||||
self.view.preservesSuperviewLayoutMargins = false
|
||||
self.view.backgroundColor = heroBackgroundColor()
|
||||
|
||||
updateContent()
|
||||
|
||||
hasLoadedView = true
|
||||
}
|
||||
|
||||
private func updateMode() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard contactShare.e164PhoneNumbers().count > 0 else {
|
||||
viewMode = .noPhoneNumber
|
||||
return
|
||||
}
|
||||
if systemContactsWithSignalAccountsForContact().count > 0 {
|
||||
viewMode = .systemContactWithSignal
|
||||
return
|
||||
}
|
||||
if systemContactsForContact().count > 0 {
|
||||
viewMode = .systemContactWithoutSignal
|
||||
return
|
||||
}
|
||||
|
||||
viewMode = .nonSystemContact
|
||||
}
|
||||
|
||||
private func systemContactsWithSignalAccountsForContact() -> [String] {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
return contactShare.systemContactsWithSignalAccountPhoneNumbers(contactsManager)
|
||||
}
|
||||
|
||||
private func systemContactsForContact() -> [String] {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
return contactShare.systemContactPhoneNumbers(contactsManager)
|
||||
}
|
||||
|
||||
private func updateContent() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let rootView = self.view else {
|
||||
owsFailDebug("missing root view.")
|
||||
return
|
||||
}
|
||||
|
||||
for subview in rootView.subviews {
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
|
||||
let topView = createTopView()
|
||||
rootView.addSubview(topView)
|
||||
topView.autoPinEdge(.top, to: .top, of: view)
|
||||
topView.autoPinWidthToSuperview()
|
||||
|
||||
// This view provides a background "below the fold".
|
||||
let bottomView = UIView.container()
|
||||
bottomView.backgroundColor = Theme.backgroundColor
|
||||
self.view.addSubview(bottomView)
|
||||
bottomView.layoutMargins = .zero
|
||||
bottomView.autoPinWidthToSuperview()
|
||||
bottomView.autoPinEdge(.top, to: .bottom, of: topView)
|
||||
bottomView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
|
||||
let scrollView = UIScrollView()
|
||||
scrollView.preservesSuperviewLayoutMargins = false
|
||||
self.view.addSubview(scrollView)
|
||||
scrollView.layoutMargins = .zero
|
||||
scrollView.autoPinWidthToSuperview()
|
||||
scrollView.autoPinEdge(.top, to: .bottom, of: topView)
|
||||
scrollView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
|
||||
let fieldsView = createFieldsView()
|
||||
|
||||
scrollView.addSubview(fieldsView)
|
||||
fieldsView.autoPinLeadingToSuperviewMargin()
|
||||
fieldsView.autoPinTrailingToSuperviewMargin()
|
||||
fieldsView.autoPinEdge(toSuperviewEdge: .top)
|
||||
fieldsView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
}
|
||||
|
||||
private func heroBackgroundColor() -> UIColor {
|
||||
return (Theme.isDarkThemeEnabled
|
||||
? UIColor(rgbHex: 0x272727)
|
||||
: UIColor(rgbHex: 0xefeff4))
|
||||
}
|
||||
|
||||
private func createTopView() -> UIView {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let topView = UIView.container()
|
||||
topView.backgroundColor = heroBackgroundColor()
|
||||
topView.preservesSuperviewLayoutMargins = false
|
||||
|
||||
// Back Button
|
||||
let backButtonSize = CGFloat(50)
|
||||
let backButton = TappableView(actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressDismiss()
|
||||
})
|
||||
backButton.autoSetDimension(.width, toSize: backButtonSize)
|
||||
backButton.autoSetDimension(.height, toSize: backButtonSize)
|
||||
topView.addSubview(backButton)
|
||||
backButton.autoPinEdge(toSuperviewEdge: .top)
|
||||
backButton.autoPinLeadingToSuperviewMargin()
|
||||
|
||||
let backIconName = (CurrentAppContext().isRTL ? "system_disclosure_indicator" : "system_disclosure_indicator_rtl")
|
||||
guard let backIconImage = UIImage(named: backIconName) else {
|
||||
owsFailDebug("missing icon.")
|
||||
return topView
|
||||
}
|
||||
let backIconView = UIImageView(image: backIconImage.withRenderingMode(.alwaysTemplate))
|
||||
backIconView.contentMode = .scaleAspectFit
|
||||
backIconView.tintColor = Theme.primaryColor.withAlphaComponent(0.6)
|
||||
backButton.addSubview(backIconView)
|
||||
backIconView.autoCenterInSuperview()
|
||||
|
||||
let avatarSize: CGFloat = 100
|
||||
let avatarView = AvatarImageView()
|
||||
avatarView.image = contactShare.getAvatarImage(diameter: avatarSize, contactsManager: contactsManager)
|
||||
topView.addSubview(avatarView)
|
||||
avatarView.autoPinEdge(toSuperviewEdge: .top, withInset: 20)
|
||||
avatarView.autoHCenterInSuperview()
|
||||
avatarView.autoSetDimension(.width, toSize: avatarSize)
|
||||
avatarView.autoSetDimension(.height, toSize: avatarSize)
|
||||
|
||||
let nameLabel = UILabel()
|
||||
nameLabel.text = contactShare.displayName
|
||||
nameLabel.font = UIFont.ows_dynamicTypeTitle1
|
||||
nameLabel.textColor = Theme.primaryColor
|
||||
nameLabel.lineBreakMode = .byTruncatingTail
|
||||
nameLabel.textAlignment = .center
|
||||
topView.addSubview(nameLabel)
|
||||
nameLabel.autoPinEdge(.top, to: .bottom, of: avatarView, withOffset: 10)
|
||||
nameLabel.autoPinLeadingToSuperviewMargin(withInset: hMargin)
|
||||
nameLabel.autoPinTrailingToSuperviewMargin(withInset: hMargin)
|
||||
|
||||
var lastView: UIView = nameLabel
|
||||
|
||||
for phoneNumber in systemContactsWithSignalAccountsForContact() {
|
||||
let phoneNumberLabel = UILabel()
|
||||
phoneNumberLabel.text = PhoneNumber.bestEffortLocalizedPhoneNumber(withE164: phoneNumber)
|
||||
phoneNumberLabel.font = UIFont.ows_dynamicTypeFootnote
|
||||
phoneNumberLabel.textColor = Theme.primaryColor
|
||||
phoneNumberLabel.lineBreakMode = .byTruncatingTail
|
||||
phoneNumberLabel.textAlignment = .center
|
||||
topView.addSubview(phoneNumberLabel)
|
||||
phoneNumberLabel.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 5)
|
||||
phoneNumberLabel.autoPinLeadingToSuperviewMargin(withInset: hMargin)
|
||||
phoneNumberLabel.autoPinTrailingToSuperviewMargin(withInset: hMargin)
|
||||
lastView = phoneNumberLabel
|
||||
}
|
||||
|
||||
switch viewMode {
|
||||
case .systemContactWithSignal:
|
||||
// Show actions buttons for system contacts with a Signal account.
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.distribution = .fillEqually
|
||||
stackView.addArrangedSubview(createCircleActionButton(text: NSLocalizedString("ACTION_SEND_MESSAGE",
|
||||
comment: "Label for 'send message' button in contact view."),
|
||||
imageName: "contact_view_message",
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressSendMessage()
|
||||
}))
|
||||
stackView.addArrangedSubview(createCircleActionButton(text: NSLocalizedString("ACTION_AUDIO_CALL",
|
||||
comment: "Label for 'audio call' button in contact view."),
|
||||
imageName: "contact_view_audio_call",
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressAudioCall()
|
||||
}))
|
||||
stackView.addArrangedSubview(createCircleActionButton(text: NSLocalizedString("ACTION_VIDEO_CALL",
|
||||
comment: "Label for 'video call' button in contact view."),
|
||||
imageName: "contact_view_video_call",
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressVideoCall()
|
||||
}))
|
||||
topView.addSubview(stackView)
|
||||
stackView.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 20)
|
||||
stackView.autoPinLeadingToSuperviewMargin(withInset: hMargin)
|
||||
stackView.autoPinTrailingToSuperviewMargin(withInset: hMargin)
|
||||
lastView = stackView
|
||||
case .systemContactWithoutSignal:
|
||||
// Show invite button for system contacts without a Signal account.
|
||||
let inviteButton = createLargePillButton(text: NSLocalizedString("ACTION_INVITE",
|
||||
comment: "Label for 'invite' button in contact view."),
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressInvite()
|
||||
})
|
||||
topView.addSubview(inviteButton)
|
||||
inviteButton.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 20)
|
||||
inviteButton.autoPinLeadingToSuperviewMargin(withInset: 55)
|
||||
inviteButton.autoPinTrailingToSuperviewMargin(withInset: 55)
|
||||
lastView = inviteButton
|
||||
case .nonSystemContact:
|
||||
// Show no action buttons for non-system contacts.
|
||||
break
|
||||
case .noPhoneNumber:
|
||||
// Show no action buttons for contacts without a phone number.
|
||||
break
|
||||
case .unknown:
|
||||
let activityIndicator = UIActivityIndicatorView(style: .whiteLarge)
|
||||
topView.addSubview(activityIndicator)
|
||||
activityIndicator.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 10)
|
||||
activityIndicator.autoHCenterInSuperview()
|
||||
lastView = activityIndicator
|
||||
break
|
||||
}
|
||||
|
||||
// Always show "add to contacts" button.
|
||||
let addToContactsButton = createLargePillButton(text: NSLocalizedString("CONVERSATION_VIEW_ADD_TO_CONTACTS_OFFER",
|
||||
comment: "Message shown in conversation view that offers to add an unknown user to your phone's contacts."),
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressAddToContacts()
|
||||
})
|
||||
topView.addSubview(addToContactsButton)
|
||||
addToContactsButton.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 20)
|
||||
addToContactsButton.autoPinLeadingToSuperviewMargin(withInset: 55)
|
||||
addToContactsButton.autoPinTrailingToSuperviewMargin(withInset: 55)
|
||||
lastView = addToContactsButton
|
||||
|
||||
lastView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 15)
|
||||
|
||||
return topView
|
||||
}
|
||||
|
||||
private func createFieldsView() -> UIView {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
var rows = [UIView]()
|
||||
|
||||
// TODO: Not designed yet.
|
||||
// if viewMode == .systemContactWithSignal ||
|
||||
// viewMode == .systemContactWithoutSignal {
|
||||
// addRow(createActionRow(labelText:NSLocalizedString("ACTION_SHARE_CONTACT",
|
||||
// comment:"Label for 'share contact' button."),
|
||||
// action:#selector(didPressShareContact)))
|
||||
// }
|
||||
|
||||
if let organizationName = contactShare.name.organizationName?.ows_stripped() {
|
||||
if (contactShare.name.hasAnyNamePart() &&
|
||||
organizationName.count > 0) {
|
||||
rows.append(ContactFieldView.contactFieldView(forOrganizationName: organizationName,
|
||||
layoutMargins: UIEdgeInsets(top: 5, left: hMargin, bottom: 5, right: hMargin)))
|
||||
}
|
||||
}
|
||||
|
||||
for phoneNumber in contactShare.phoneNumbers {
|
||||
rows.append(ContactFieldView.contactFieldView(forPhoneNumber: phoneNumber,
|
||||
layoutMargins: UIEdgeInsets(top: 5, left: hMargin, bottom: 5, right: hMargin),
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressPhoneNumber(phoneNumber: phoneNumber)
|
||||
}))
|
||||
}
|
||||
|
||||
for email in contactShare.emails {
|
||||
rows.append(ContactFieldView.contactFieldView(forEmail: email,
|
||||
layoutMargins: UIEdgeInsets(top: 5, left: hMargin, bottom: 5, right: hMargin),
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressEmail(email: email)
|
||||
}))
|
||||
}
|
||||
|
||||
for address in contactShare.addresses {
|
||||
rows.append(ContactFieldView.contactFieldView(forAddress: address,
|
||||
layoutMargins: UIEdgeInsets(top: 5, left: hMargin, bottom: 5, right: hMargin),
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressAddress(address: address)
|
||||
}))
|
||||
}
|
||||
|
||||
return ContactFieldView(rows: rows, hMargin: hMargin)
|
||||
}
|
||||
|
||||
private let hMargin = CGFloat(16)
|
||||
|
||||
private func createActionRow(labelText: String, action: Selector) -> UIView {
|
||||
let row = UIView()
|
||||
row.layoutMargins.left = 0
|
||||
row.layoutMargins.right = 0
|
||||
row.isUserInteractionEnabled = true
|
||||
row.addGestureRecognizer(UITapGestureRecognizer(target: self, action: action))
|
||||
|
||||
let label = UILabel()
|
||||
label.text = labelText
|
||||
label.font = UIFont.ows_dynamicTypeBody
|
||||
label.textColor = UIColor.ows_materialBlue
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
row.addSubview(label)
|
||||
label.autoPinTopToSuperviewMargin()
|
||||
label.autoPinBottomToSuperviewMargin()
|
||||
label.autoPinLeadingToSuperviewMargin(withInset: hMargin)
|
||||
label.autoPinTrailingToSuperviewMargin(withInset: hMargin)
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
// TODO: Use real assets.
|
||||
private func createCircleActionButton(text: String, imageName: String, actionBlock : @escaping () -> Void) -> UIView {
|
||||
let buttonSize = CGFloat(50)
|
||||
|
||||
let button = TappableView(actionBlock: actionBlock)
|
||||
button.layoutMargins = .zero
|
||||
button.autoSetDimension(.width, toSize: buttonSize, relation: .greaterThanOrEqual)
|
||||
|
||||
let circleView = UIView()
|
||||
circleView.backgroundColor = Theme.backgroundColor
|
||||
circleView.autoSetDimension(.width, toSize: buttonSize)
|
||||
circleView.autoSetDimension(.height, toSize: buttonSize)
|
||||
circleView.layer.cornerRadius = buttonSize * 0.5
|
||||
button.addSubview(circleView)
|
||||
circleView.autoPinEdge(toSuperviewEdge: .top)
|
||||
circleView.autoHCenterInSuperview()
|
||||
|
||||
guard let image = UIImage(named: imageName) else {
|
||||
owsFailDebug("missing image.")
|
||||
return button
|
||||
}
|
||||
let imageView = UIImageView(image: image.withRenderingMode(.alwaysTemplate))
|
||||
imageView.tintColor = Theme.primaryColor.withAlphaComponent(0.6)
|
||||
circleView.addSubview(imageView)
|
||||
imageView.autoCenterInSuperview()
|
||||
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.font = UIFont.ows_dynamicTypeCaption2
|
||||
label.textColor = Theme.primaryColor
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.textAlignment = .center
|
||||
button.addSubview(label)
|
||||
label.autoPinEdge(.top, to: .bottom, of: circleView, withOffset: 3)
|
||||
label.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
label.autoPinLeadingToSuperviewMargin()
|
||||
label.autoPinTrailingToSuperviewMargin()
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
private func createLargePillButton(text: String, actionBlock : @escaping () -> Void) -> UIView {
|
||||
let button = TappableView(actionBlock: actionBlock)
|
||||
button.backgroundColor = Theme.backgroundColor
|
||||
button.layoutMargins = .zero
|
||||
button.autoSetDimension(.height, toSize: 45)
|
||||
button.layer.cornerRadius = 5
|
||||
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.font = UIFont.ows_dynamicTypeBody
|
||||
label.textColor = UIColor.ows_materialBlue
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.textAlignment = .center
|
||||
button.addSubview(label)
|
||||
label.autoPinLeadingToSuperviewMargin(withInset: 20)
|
||||
label.autoPinTrailingToSuperviewMargin(withInset: 20)
|
||||
label.autoVCenterInSuperview()
|
||||
label.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual)
|
||||
label.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual)
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
func didPressShareContact(sender: UIGestureRecognizer) {
|
||||
Logger.info("")
|
||||
|
||||
guard sender.state == .recognized else {
|
||||
return
|
||||
}
|
||||
// TODO:
|
||||
}
|
||||
|
||||
func didPressSendMessage() {
|
||||
Logger.info("")
|
||||
|
||||
self.contactShareViewHelper.sendMessage(contactShare: self.contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didPressAudioCall() {
|
||||
Logger.info("")
|
||||
|
||||
self.contactShareViewHelper.audioCall(contactShare: self.contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didPressVideoCall() {
|
||||
Logger.info("")
|
||||
|
||||
self.contactShareViewHelper.videoCall(contactShare: self.contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didPressInvite() {
|
||||
Logger.info("")
|
||||
|
||||
self.contactShareViewHelper.showInviteContact(contactShare: self.contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didPressAddToContacts() {
|
||||
Logger.info("")
|
||||
|
||||
self.contactShareViewHelper.showAddToContacts(contactShare: self.contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didPressDismiss() {
|
||||
Logger.info("")
|
||||
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
navigationController.popViewController(animated: true)
|
||||
}
|
||||
|
||||
func didPressPhoneNumber(phoneNumber: OWSContactPhoneNumber) {
|
||||
Logger.info("")
|
||||
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
if let e164 = phoneNumber.tryToConvertToE164() {
|
||||
if contactShare.systemContactsWithSignalAccountPhoneNumbers(contactsManager).contains(e164) {
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ACTION_SEND_MESSAGE",
|
||||
comment: "Label for 'send message' button in contact view."),
|
||||
style: .default) { _ in
|
||||
SignalApp.shared().presentConversation(forRecipientId: e164, action: .compose, animated: true)
|
||||
})
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ACTION_AUDIO_CALL",
|
||||
comment: "Label for 'audio call' button in contact view."),
|
||||
style: .default) { _ in
|
||||
SignalApp.shared().presentConversation(forRecipientId: e164, action: .audioCall, animated: true)
|
||||
})
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ACTION_VIDEO_CALL",
|
||||
comment: "Label for 'video call' button in contact view."),
|
||||
style: .default) { _ in
|
||||
SignalApp.shared().presentConversation(forRecipientId: e164, action: .videoCall, animated: true)
|
||||
})
|
||||
} else {
|
||||
// TODO: We could offer callPhoneNumberWithSystemCall.
|
||||
}
|
||||
}
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("EDIT_ITEM_COPY_ACTION",
|
||||
comment: "Short name for edit menu item to copy contents of media message."),
|
||||
style: .default) { _ in
|
||||
UIPasteboard.general.string = phoneNumber.phoneNumber
|
||||
})
|
||||
actionSheet.addAction(OWSAlerts.cancelAction)
|
||||
presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
func callPhoneNumberWithSystemCall(phoneNumber: OWSContactPhoneNumber) {
|
||||
Logger.info("")
|
||||
|
||||
guard let url = NSURL(string: "tel:\(phoneNumber.phoneNumber)") else {
|
||||
owsFailDebug("could not open phone number.")
|
||||
return
|
||||
}
|
||||
UIApplication.shared.openURL(url as URL)
|
||||
}
|
||||
|
||||
func didPressEmail(email: OWSContactEmail) {
|
||||
Logger.info("")
|
||||
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("CONTACT_VIEW_OPEN_EMAIL_IN_EMAIL_APP",
|
||||
comment: "Label for 'open email in email app' button in contact view."),
|
||||
style: .default) { [weak self] _ in
|
||||
self?.openEmailInEmailApp(email: email)
|
||||
})
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("EDIT_ITEM_COPY_ACTION",
|
||||
comment: "Short name for edit menu item to copy contents of media message."),
|
||||
style: .default) { _ in
|
||||
UIPasteboard.general.string = email.email
|
||||
})
|
||||
actionSheet.addAction(OWSAlerts.cancelAction)
|
||||
presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
func openEmailInEmailApp(email: OWSContactEmail) {
|
||||
Logger.info("")
|
||||
|
||||
guard let url = NSURL(string: "mailto:\(email.email)") else {
|
||||
owsFailDebug("could not open email.")
|
||||
return
|
||||
}
|
||||
UIApplication.shared.openURL(url as URL)
|
||||
}
|
||||
|
||||
func didPressAddress(address: OWSContactAddress) {
|
||||
Logger.info("")
|
||||
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("CONTACT_VIEW_OPEN_ADDRESS_IN_MAPS_APP",
|
||||
comment: "Label for 'open address in maps app' button in contact view."),
|
||||
style: .default) { [weak self] _ in
|
||||
self?.openAddressInMaps(address: address)
|
||||
})
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("EDIT_ITEM_COPY_ACTION",
|
||||
comment: "Short name for edit menu item to copy contents of media message."),
|
||||
style: .default) { [weak self] _ in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
UIPasteboard.general.string = strongSelf.formatAddressForQuery(address: address)
|
||||
})
|
||||
actionSheet.addAction(OWSAlerts.cancelAction)
|
||||
presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
func openAddressInMaps(address: OWSContactAddress) {
|
||||
Logger.info("")
|
||||
|
||||
let mapAddress = formatAddressForQuery(address: address)
|
||||
guard let escapedMapAddress = mapAddress.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
|
||||
owsFailDebug("could not open address.")
|
||||
return
|
||||
}
|
||||
// Note that we use "q" (i.e. query) rather than "address" since we can't assume
|
||||
// this is a well-formed address.
|
||||
guard let url = URL(string: "http://maps.apple.com/?q=\(escapedMapAddress)") else {
|
||||
owsFailDebug("could not open address.")
|
||||
return
|
||||
}
|
||||
|
||||
UIApplication.shared.openURL(url as URL)
|
||||
}
|
||||
|
||||
func formatAddressForQuery(address: OWSContactAddress) -> String {
|
||||
Logger.info("")
|
||||
|
||||
// Open address in Apple Maps app.
|
||||
var addressParts = [String]()
|
||||
let addAddressPart: ((String?) -> Void) = { (part) in
|
||||
guard let part = part else {
|
||||
return
|
||||
}
|
||||
guard part.count > 0 else {
|
||||
return
|
||||
}
|
||||
addressParts.append(part)
|
||||
}
|
||||
addAddressPart(address.street)
|
||||
addAddressPart(address.neighborhood)
|
||||
addAddressPart(address.city)
|
||||
addAddressPart(address.region)
|
||||
addAddressPart(address.postcode)
|
||||
addAddressPart(address.country)
|
||||
return addressParts.joined(separator: ", ")
|
||||
}
|
||||
|
||||
// MARK: - ContactShareViewHelperDelegate
|
||||
|
||||
public func didCreateOrEditContact() {
|
||||
Logger.info("")
|
||||
updateContent()
|
||||
|
||||
self.dismiss(animated: true)
|
||||
}
|
||||
}
|
|
@ -1,404 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
// Originally based on EPContacts
|
||||
//
|
||||
// Created by Prabaharan Elangovan on 12/10/15.
|
||||
// Parts Copyright © 2015 Prabaharan Elangovan. All rights reserved
|
||||
|
||||
import UIKit
|
||||
import Contacts
|
||||
import SignalUtilitiesKit
|
||||
|
||||
@objc
|
||||
public protocol ContactsPickerDelegate: class {
|
||||
func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError)
|
||||
func contactsPickerDidCancel(_: ContactsPicker)
|
||||
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact)
|
||||
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact])
|
||||
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool
|
||||
}
|
||||
|
||||
@objc
|
||||
public enum SubtitleCellValue: Int {
|
||||
case phoneNumber, email, none
|
||||
}
|
||||
|
||||
@objc
|
||||
public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate {
|
||||
|
||||
var tableView: UITableView!
|
||||
var searchBar: UISearchBar!
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private let contactCellReuseIdentifier = "contactCellReuseIdentifier"
|
||||
|
||||
private var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
// HACK: Though we don't have an input accessory view, the VC we are presented above (ConversationVC) does.
|
||||
// If the app is backgrounded and then foregrounded, when OWSWindowManager calls mainWindow.makeKeyAndVisible
|
||||
// the ConversationVC's inputAccessoryView will appear *above* us unless we'd previously become first responder.
|
||||
override public var canBecomeFirstResponder: Bool {
|
||||
Logger.debug("")
|
||||
return true
|
||||
}
|
||||
|
||||
override public func becomeFirstResponder() -> Bool {
|
||||
Logger.debug("")
|
||||
return super.becomeFirstResponder()
|
||||
}
|
||||
|
||||
override public func resignFirstResponder() -> Bool {
|
||||
Logger.debug("")
|
||||
return super.resignFirstResponder()
|
||||
}
|
||||
|
||||
private let collation = UILocalizedIndexedCollation.current()
|
||||
public var collationForTests: UILocalizedIndexedCollation {
|
||||
get {
|
||||
return collation
|
||||
}
|
||||
}
|
||||
private let contactStore = CNContactStore()
|
||||
|
||||
// Data Source State
|
||||
private lazy var sections = [[CNContact]]()
|
||||
private lazy var filteredSections = [[CNContact]]()
|
||||
private lazy var selectedContacts = [Contact]()
|
||||
|
||||
// Configuration
|
||||
@objc
|
||||
public weak var contactsPickerDelegate: ContactsPickerDelegate?
|
||||
private let subtitleCellType: SubtitleCellValue
|
||||
private let allowsMultipleSelection: Bool
|
||||
private let allowedContactKeys: [CNKeyDescriptor] = ContactsFrameworkContactStoreAdaptee.allowedContactKeys
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
@objc
|
||||
required public init(allowsMultipleSelection: Bool, subtitleCellType: SubtitleCellValue) {
|
||||
self.allowsMultipleSelection = allowsMultipleSelection
|
||||
self.subtitleCellType = subtitleCellType
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle Methods
|
||||
|
||||
override public func loadView() {
|
||||
self.view = UIView()
|
||||
let tableView = UITableView()
|
||||
self.tableView = tableView
|
||||
self.tableView.separatorColor = Theme.cellSeparatorColor
|
||||
|
||||
view.addSubview(tableView)
|
||||
tableView.autoPinEdge(toSuperviewEdge: .top)
|
||||
tableView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
tableView.autoPinEdge(toSuperviewSafeArea: .leading)
|
||||
tableView.autoPinEdge(toSuperviewSafeArea: .trailing)
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
|
||||
let searchBar = OWSSearchBar()
|
||||
self.searchBar = searchBar
|
||||
searchBar.delegate = self
|
||||
searchBar.sizeToFit()
|
||||
|
||||
tableView.tableHeaderView = searchBar
|
||||
}
|
||||
|
||||
override open func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.view.backgroundColor = Theme.backgroundColor
|
||||
self.tableView.backgroundColor = Theme.backgroundColor
|
||||
|
||||
searchBar.placeholder = NSLocalizedString("INVITE_FRIENDS_PICKER_SEARCHBAR_PLACEHOLDER", comment: "Search")
|
||||
|
||||
// Auto size cells for dynamic type
|
||||
tableView.estimatedRowHeight = 60.0
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 60
|
||||
|
||||
tableView.allowsMultipleSelection = allowsMultipleSelection
|
||||
|
||||
tableView.separatorInset = UIEdgeInsets(top: 0, left: ContactCell.kSeparatorHInset, bottom: 0, right: 16)
|
||||
|
||||
registerContactCell()
|
||||
initializeBarButtons()
|
||||
reloadContacts()
|
||||
updateSearchResults(searchText: "")
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.didChangePreferredContentSize), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
public func didChangePreferredContentSize() {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
|
||||
private func initializeBarButtons() {
|
||||
let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(onTouchCancelButton))
|
||||
self.navigationItem.leftBarButtonItem = cancelButton
|
||||
|
||||
if allowsMultipleSelection {
|
||||
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(onTouchDoneButton))
|
||||
self.navigationItem.rightBarButtonItem = doneButton
|
||||
}
|
||||
}
|
||||
|
||||
private func registerContactCell() {
|
||||
tableView.register(ContactCell.self, forCellReuseIdentifier: contactCellReuseIdentifier)
|
||||
}
|
||||
|
||||
// MARK: - Contact Operations
|
||||
|
||||
private func reloadContacts() {
|
||||
getContacts( onError: { error in
|
||||
Logger.error("failed to reload contacts with error:\(error)")
|
||||
})
|
||||
}
|
||||
|
||||
private func getContacts(onError errorHandler: @escaping (_ error: Error) -> Void) {
|
||||
switch CNContactStore.authorizationStatus(for: CNEntityType.contacts) {
|
||||
case CNAuthorizationStatus.denied, CNAuthorizationStatus.restricted:
|
||||
let title = NSLocalizedString("INVITE_FLOW_REQUIRES_CONTACT_ACCESS_TITLE", comment: "Alert title when contacts disabled while trying to invite contacts to signal")
|
||||
let body = NSLocalizedString("INVITE_FLOW_REQUIRES_CONTACT_ACCESS_BODY", comment: "Alert body when contacts disabled while trying to invite contacts to signal")
|
||||
|
||||
let alert = UIAlertController(title: title, message: body, preferredStyle: .alert)
|
||||
|
||||
let dismissText = CommonStrings.cancelButton
|
||||
|
||||
let cancelAction = UIAlertAction(title: dismissText, style: .cancel, handler: { _ in
|
||||
let error = NSError(domain: "contactsPickerErrorDomain", code: 1, userInfo: [NSLocalizedDescriptionKey: "No Contacts Access"])
|
||||
self.contactsPickerDelegate?.contactsPicker(self, contactFetchDidFail: error)
|
||||
errorHandler(error)
|
||||
})
|
||||
alert.addAction(cancelAction)
|
||||
|
||||
let settingsText = CommonStrings.openSettingsButton
|
||||
let openSettingsAction = UIAlertAction(title: settingsText, style: .default, handler: { (_) in
|
||||
UIApplication.shared.openSystemSettings()
|
||||
})
|
||||
alert.addAction(openSettingsAction)
|
||||
|
||||
self.presentAlert(alert)
|
||||
|
||||
case CNAuthorizationStatus.notDetermined:
|
||||
//This case means the user is prompted for the first time for allowing contacts
|
||||
contactStore.requestAccess(for: CNEntityType.contacts) { (granted, error) -> Void in
|
||||
//At this point an alert is provided to the user to provide access to contacts. This will get invoked if a user responds to the alert
|
||||
if granted {
|
||||
self.getContacts(onError: errorHandler)
|
||||
} else {
|
||||
errorHandler(error!)
|
||||
}
|
||||
}
|
||||
|
||||
case CNAuthorizationStatus.authorized:
|
||||
//Authorization granted by user for this app.
|
||||
var contacts = [CNContact]()
|
||||
|
||||
do {
|
||||
let contactFetchRequest = CNContactFetchRequest(keysToFetch: allowedContactKeys)
|
||||
contactFetchRequest.sortOrder = .userDefault
|
||||
try contactStore.enumerateContacts(with: contactFetchRequest) { (contact, _) -> Void in
|
||||
contacts.append(contact)
|
||||
}
|
||||
self.sections = collatedContacts(contacts)
|
||||
} catch let error as NSError {
|
||||
Logger.error("Failed to fetch contacts with error:\(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func collatedContacts(_ contacts: [CNContact]) -> [[CNContact]] {
|
||||
let selector: Selector = #selector(getter: CNContact.nameForCollating)
|
||||
|
||||
var collated = Array(repeating: [CNContact](), count: collation.sectionTitles.count)
|
||||
for contact in contacts {
|
||||
let sectionNumber = collation.section(for: contact, collationStringSelector: selector)
|
||||
collated[sectionNumber].append(contact)
|
||||
}
|
||||
return collated
|
||||
}
|
||||
|
||||
// MARK: - Table View DataSource
|
||||
|
||||
open func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return self.collation.sectionTitles.count
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let dataSource = filteredSections
|
||||
|
||||
guard section < dataSource.count else {
|
||||
return 0
|
||||
}
|
||||
|
||||
return dataSource[section].count
|
||||
}
|
||||
|
||||
// MARK: - Table View Delegates
|
||||
|
||||
open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: contactCellReuseIdentifier, for: indexPath) as? ContactCell else {
|
||||
owsFailDebug("cell had unexpected type")
|
||||
return UITableViewCell()
|
||||
}
|
||||
|
||||
let dataSource = filteredSections
|
||||
let cnContact = dataSource[indexPath.section][indexPath.row]
|
||||
let contact = Contact(systemContact: cnContact)
|
||||
|
||||
cell.configure(contact: contact, subtitleType: subtitleCellType, showsWhenSelected: self.allowsMultipleSelection, contactsManager: self.contactsManager)
|
||||
let isSelected = selectedContacts.contains(where: { $0.uniqueId == contact.uniqueId })
|
||||
cell.isSelected = isSelected
|
||||
|
||||
// Make sure we preserve selection across tableView.reloadData which happens when toggling between
|
||||
// search controller
|
||||
if (isSelected) {
|
||||
self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
|
||||
} else {
|
||||
self.tableView.deselectRow(at: indexPath, animated: false)
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
|
||||
let cell = tableView.cellForRow(at: indexPath) as! ContactCell
|
||||
let deselectedContact = cell.contact!
|
||||
|
||||
selectedContacts = selectedContacts.filter {
|
||||
return $0.uniqueId != deselectedContact.uniqueId
|
||||
}
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
Logger.verbose("")
|
||||
|
||||
let cell = tableView.cellForRow(at: indexPath) as! ContactCell
|
||||
let selectedContact = cell.contact!
|
||||
|
||||
guard (contactsPickerDelegate == nil || contactsPickerDelegate!.contactsPicker(self, shouldSelectContact: selectedContact)) else {
|
||||
self.tableView.deselectRow(at: indexPath, animated: false)
|
||||
return
|
||||
}
|
||||
|
||||
selectedContacts.append(selectedContact)
|
||||
|
||||
if !allowsMultipleSelection {
|
||||
// Single selection code
|
||||
self.contactsPickerDelegate?.contactsPicker(self, didSelectContact: selectedContact)
|
||||
}
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
|
||||
return collation.section(forSectionIndexTitle: index)
|
||||
}
|
||||
|
||||
open func sectionIndexTitles(for tableView: UITableView) -> [String]? {
|
||||
return collation.sectionIndexTitles
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
let dataSource = filteredSections
|
||||
|
||||
guard section < dataSource.count else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Don't show empty sections
|
||||
if dataSource[section].count > 0 {
|
||||
guard section < collation.sectionTitles.count else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return collation.sectionTitles[section]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Button Actions
|
||||
|
||||
@objc func onTouchCancelButton() {
|
||||
contactsPickerDelegate?.contactsPickerDidCancel(self)
|
||||
}
|
||||
|
||||
@objc func onTouchDoneButton() {
|
||||
contactsPickerDelegate?.contactsPicker(self, didSelectMultipleContacts: selectedContacts)
|
||||
}
|
||||
|
||||
// MARK: - Search Actions
|
||||
open func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
updateSearchResults(searchText: searchText)
|
||||
}
|
||||
|
||||
open func updateSearchResults(searchText: String) {
|
||||
let predicate: NSPredicate
|
||||
if searchText.isEmpty {
|
||||
filteredSections = sections
|
||||
} else {
|
||||
do {
|
||||
predicate = CNContact.predicateForContacts(matchingName: searchText)
|
||||
let filteredContacts = try contactStore.unifiedContacts(matching: predicate, keysToFetch: allowedContactKeys)
|
||||
filteredSections = collatedContacts(filteredContacts)
|
||||
} catch let error as NSError {
|
||||
Logger.error("updating search results failed with error: \(error)")
|
||||
}
|
||||
}
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
let ContactSortOrder = computeSortOrder()
|
||||
|
||||
func computeSortOrder() -> CNContactSortOrder {
|
||||
let comparator = CNContact.comparator(forNameSortOrder: .userDefault)
|
||||
|
||||
let contact0 = CNMutableContact()
|
||||
contact0.givenName = "A"
|
||||
contact0.familyName = "Z"
|
||||
|
||||
let contact1 = CNMutableContact()
|
||||
contact1.givenName = "Z"
|
||||
contact1.familyName = "A"
|
||||
|
||||
let result = comparator(contact0, contact1)
|
||||
|
||||
if result == .orderedAscending {
|
||||
return .givenName
|
||||
} else {
|
||||
return .familyName
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension CNContact {
|
||||
/**
|
||||
* Sorting Key used by collation
|
||||
*/
|
||||
@objc var nameForCollating: String {
|
||||
get {
|
||||
if self.familyName.isEmpty && self.givenName.isEmpty {
|
||||
return self.emailAddresses.first?.value as String? ?? ""
|
||||
}
|
||||
|
||||
let compositeName: String
|
||||
if ContactSortOrder == .familyName {
|
||||
compositeName = "\(self.familyName) \(self.givenName)"
|
||||
} else {
|
||||
compositeName = "\(self.givenName) \(self.familyName)"
|
||||
}
|
||||
return compositeName.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc
|
||||
class ConversationConfigurationSyncOperation: OWSOperation {
|
||||
|
||||
enum ColorSyncOperationError: Error {
|
||||
case assertionError(description: String)
|
||||
}
|
||||
|
||||
private var dbConnection: YapDatabaseConnection {
|
||||
return OWSPrimaryStorage.shared().dbReadConnection
|
||||
}
|
||||
|
||||
private var messageSenderJobQueue: MessageSenderJobQueue {
|
||||
return SSKEnvironment.shared.messageSenderJobQueue
|
||||
}
|
||||
|
||||
private var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
private var syncManager: OWSSyncManagerProtocol {
|
||||
return SSKEnvironment.shared.syncManager
|
||||
}
|
||||
|
||||
private let thread: TSThread
|
||||
|
||||
@objc
|
||||
public init(thread: TSThread) {
|
||||
self.thread = thread
|
||||
super.init()
|
||||
}
|
||||
|
||||
override public func run() {
|
||||
if let contactThread = thread as? TSContactThread {
|
||||
sync(contactThread: contactThread)
|
||||
} else if let groupThread = thread as? TSGroupThread {
|
||||
sync(groupThread: groupThread)
|
||||
} else {
|
||||
self.reportAssertionError(description: "unknown thread type")
|
||||
}
|
||||
}
|
||||
|
||||
private func reportAssertionError(description: String) {
|
||||
let error = ColorSyncOperationError.assertionError(description: description)
|
||||
self.reportError(error)
|
||||
}
|
||||
|
||||
private func sync(contactThread: TSContactThread) {
|
||||
guard let signalAccount: SignalAccount = self.contactsManager.fetchSignalAccount(forRecipientId: contactThread.contactIdentifier()) else {
|
||||
reportAssertionError(description: "unable to find signalAccount")
|
||||
return
|
||||
}
|
||||
|
||||
syncManager.syncContacts(for: [signalAccount]).retainUntilComplete()
|
||||
}
|
||||
|
||||
private func sync(groupThread: TSGroupThread) {
|
||||
// TODO sync only the affected group
|
||||
// The current implementation works, but seems wasteful.
|
||||
// Does desktop handle single group sync correctly?
|
||||
// What does Android do?
|
||||
let syncMessage: OWSSyncGroupsMessage = OWSSyncGroupsMessage(groupThread: groupThread)
|
||||
|
||||
var dataSource: DataSource?
|
||||
self.dbConnection.read { transaction in
|
||||
guard let messageData: Data = syncMessage.buildPlainTextAttachmentData(with: transaction) else {
|
||||
owsFailDebug("could not serialize sync groups data")
|
||||
return
|
||||
}
|
||||
dataSource = DataSourceValue.dataSource(withSyncMessageData: messageData)
|
||||
}
|
||||
|
||||
guard let attachmentDataSource = dataSource else {
|
||||
self.reportAssertionError(description: "unable to build attachment data source")
|
||||
return
|
||||
}
|
||||
|
||||
self.sendConfiguration(attachmentDataSource: attachmentDataSource, syncMessage: syncMessage)
|
||||
}
|
||||
|
||||
private func sendConfiguration(attachmentDataSource: DataSource, syncMessage: OWSOutgoingSyncMessage) {
|
||||
self.messageSenderJobQueue.add(mediaMessage: syncMessage,
|
||||
dataSource: attachmentDataSource,
|
||||
contentType: OWSMimeTypeApplicationOctetStream,
|
||||
sourceFilename: nil,
|
||||
caption: nil,
|
||||
albumMessageId: nil,
|
||||
isTemporaryAttachment: true)
|
||||
self.reportSuccess()
|
||||
}
|
||||
|
||||
}
|
|
@ -6,10 +6,10 @@
|
|||
#import "OWSBezierPathView.h"
|
||||
#import "OWSProgressView.h"
|
||||
#import <SignalUtilitiesKit/UIFont+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SignalUtilitiesKit/AppContext.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/AppContext.h>
|
||||
#import <SignalUtilitiesKit/OWSUploadOperation.h>
|
||||
#import <SignalUtilitiesKit/TSAttachmentStream.h>
|
||||
#import <SessionMessagingKit/TSAttachmentStream.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -13,12 +13,6 @@ public class ConversationMediaView: UIView {
|
|||
case failed
|
||||
}
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private var attachmentDownloads: OWSAttachmentDownloads {
|
||||
return SSKEnvironment.shared.attachmentDownloads
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private let mediaCache: NSCache<NSString, AnyObject>
|
||||
|
@ -163,11 +157,13 @@ public class ConversationMediaView: UIView {
|
|||
configure(forError: .invalid)
|
||||
return
|
||||
}
|
||||
/*
|
||||
guard nil != attachmentDownloads.downloadProgress(forAttachmentId: attachmentId) else {
|
||||
// Not being downloaded.
|
||||
configure(forError: .missing)
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
||||
backgroundColor = (Theme.isDarkThemeEnabled ? .ows_gray90 : .ows_gray05)
|
||||
let view: UIView
|
||||
|
|
|
@ -35,23 +35,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
#pragma mark - System Cell
|
||||
|
||||
- (void)tappedNonBlockingIdentityChangeForRecipientId:(nullable NSString *)signalId;
|
||||
- (void)tappedInvalidIdentityKeyErrorMessage:(TSInvalidIdentityKeyErrorMessage *)errorMessage;
|
||||
- (void)tappedCorruptedMessage:(TSErrorMessage *)message;
|
||||
- (void)resendGroupUpdateForErrorMessage:(TSErrorMessage *)message;
|
||||
- (void)showFingerprintWithRecipientId:(NSString *)recipientId;
|
||||
- (void)showConversationSettings;
|
||||
- (void)handleCallTap:(TSCall *)call;
|
||||
|
||||
#pragma mark - Offers
|
||||
|
||||
- (void)tappedUnknownContactBlockOfferMessage:(OWSContactOffersInteraction *)interaction;
|
||||
- (void)tappedAddToContactsOfferMessage:(OWSContactOffersInteraction *)interaction;
|
||||
- (void)tappedAddToProfileWhitelistOfferMessage:(OWSContactOffersInteraction *)interaction;
|
||||
|
||||
#pragma mark - Formatting
|
||||
|
||||
- (NSAttributedString *)attributedContactOrProfileNameForPhoneIdentifier:(NSString *)recipientId;
|
||||
|
||||
#pragma mark - Caching
|
||||
|
||||
|
@ -61,10 +47,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (void)didTapFailedOutgoingMessage:(TSOutgoingMessage *)message;
|
||||
|
||||
#pragma mark - Contacts
|
||||
|
||||
- (OWSContactsManager *)contactsManager;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
|
|
@ -7,12 +7,6 @@ import Foundation
|
|||
@objc
|
||||
public class MediaDownloadView: UIView {
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private var attachmentDownloads: OWSAttachmentDownloads {
|
||||
return SSKEnvironment.shared.attachmentDownloads
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private let attachmentId: String
|
||||
|
@ -75,13 +69,13 @@ public class MediaDownloadView: UIView {
|
|||
shapeLayer1.frame = self.bounds
|
||||
shapeLayer2.frame = self.bounds
|
||||
|
||||
guard let progress = attachmentDownloads.downloadProgress(forAttachmentId: attachmentId) else {
|
||||
Logger.warn("No progress for attachment.")
|
||||
shapeLayer1.path = nil
|
||||
shapeLayer2.path = nil
|
||||
return
|
||||
}
|
||||
shapeLayer1.path = nil
|
||||
shapeLayer2.path = nil
|
||||
return
|
||||
|
||||
// We can't display download progress yet
|
||||
|
||||
/*
|
||||
// Prevent the shape layer from animating changes.
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
|
@ -115,5 +109,6 @@ public class MediaDownloadView: UIView {
|
|||
shapeLayer2.fillColor = fillColor2.cgColor
|
||||
|
||||
CATransaction.commit()
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
#import "OWSBubbleShapeView.h"
|
||||
#import "OWSBubbleView.h"
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
#import "OWSBubbleView.h"
|
||||
#import "MainAppContext.h"
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
#import "Session-Swift.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConversationViewCell.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class OWSContactOffersInteraction;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSContactOffersCell : ConversationViewCell
|
||||
|
||||
+ (NSString *)cellReuseIdentifier;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,263 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSContactOffersCell.h"
|
||||
#import "ConversationViewItem.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalUtilitiesKit/OWSContactOffersInteraction.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIFont+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSContactOffersCell ()
|
||||
|
||||
@property (nonatomic) UILabel *titleLabel;
|
||||
@property (nonatomic) UIButton *addToContactsButton;
|
||||
@property (nonatomic) UIButton *addToProfileWhitelistButton;
|
||||
@property (nonatomic) UIButton *blockButton;
|
||||
@property (nonatomic) NSArray<NSLayoutConstraint *> *layoutConstraints;
|
||||
@property (nonatomic) UIStackView *stackView;
|
||||
@property (nonatomic) UIStackView *buttonStackView;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSContactOffersCell
|
||||
|
||||
// `[UIView init]` invokes `[self initWithFrame:...]`.
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
[self commontInit];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)commontInit
|
||||
{
|
||||
OWSAssertDebug(!self.titleLabel);
|
||||
|
||||
self.layoutMargins = UIEdgeInsetsZero;
|
||||
self.contentView.layoutMargins = UIEdgeInsetsZero;
|
||||
self.layoutConstraints = @[];
|
||||
|
||||
self.titleLabel = [UILabel new];
|
||||
self.titleLabel.text = NSLocalizedString(@"CONVERSATION_VIEW_CONTACTS_OFFER_TITLE",
|
||||
@"Title for the group of buttons show for unknown contacts offering to add them to contacts, etc.");
|
||||
self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
self.titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
|
||||
self.addToContactsButton = [self
|
||||
createButtonWithTitle:
|
||||
NSLocalizedString(@"CONVERSATION_VIEW_ADD_TO_CONTACTS_OFFER",
|
||||
@"Message shown in conversation view that offers to add an unknown user to your phone's contacts.")
|
||||
selector:@selector(addToContacts)];
|
||||
self.addToProfileWhitelistButton = [self
|
||||
createButtonWithTitle:NSLocalizedString(@"CONVERSATION_VIEW_ADD_USER_TO_PROFILE_WHITELIST_OFFER",
|
||||
@"Message shown in conversation view that offers to share your profile with a user.")
|
||||
selector:@selector(addToProfileWhitelist)];
|
||||
self.blockButton =
|
||||
[self createButtonWithTitle:NSLocalizedString(@"CONVERSATION_VIEW_UNKNOWN_CONTACT_BLOCK_OFFER",
|
||||
@"Message shown in conversation view that offers to block an unknown user.")
|
||||
selector:@selector(block)];
|
||||
|
||||
UIStackView *buttonStackView = [[UIStackView alloc] initWithArrangedSubviews:self.buttons];
|
||||
buttonStackView.axis = UILayoutConstraintAxisVertical;
|
||||
buttonStackView.spacing = self.vSpacing;
|
||||
self.buttonStackView = buttonStackView;
|
||||
|
||||
self.stackView = [[UIStackView alloc] initWithArrangedSubviews:@[
|
||||
self.titleLabel,
|
||||
buttonStackView,
|
||||
]];
|
||||
self.stackView.axis = UILayoutConstraintAxisVertical;
|
||||
self.stackView.spacing = self.vSpacing;
|
||||
self.stackView.alignment = UIStackViewAlignmentCenter;
|
||||
[self.contentView addSubview:self.stackView];
|
||||
}
|
||||
|
||||
- (void)configureFonts
|
||||
{
|
||||
self.titleLabel.font = UIFont.ows_dynamicTypeSubheadlineFont;
|
||||
|
||||
UIFont *buttonFont = UIFont.ows_dynamicTypeSubheadlineFont.ows_mediumWeight;
|
||||
self.addToContactsButton.titleLabel.font = buttonFont;
|
||||
self.addToProfileWhitelistButton.titleLabel.font = buttonFont;
|
||||
self.blockButton.titleLabel.font = buttonFont;
|
||||
}
|
||||
|
||||
- (UIButton *)createButtonWithTitle:(NSString *)title selector:(SEL)selector
|
||||
{
|
||||
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[button setTitle:title forState:UIControlStateNormal];
|
||||
button.titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
button.layer.cornerRadius = 4.f;
|
||||
[button addTarget:self action:selector forControlEvents:UIControlEventTouchUpInside];
|
||||
button.contentEdgeInsets = UIEdgeInsetsMake(0, 10.f, 0, 10.f);
|
||||
return button;
|
||||
}
|
||||
|
||||
+ (NSString *)cellReuseIdentifier
|
||||
{
|
||||
return NSStringFromClass([self class]);
|
||||
}
|
||||
|
||||
- (void)loadForDisplay
|
||||
{
|
||||
OWSAssertDebug(self.conversationStyle);
|
||||
OWSAssertDebug(self.conversationStyle.viewWidth > 0);
|
||||
OWSAssertDebug(self.viewItem);
|
||||
OWSAssertDebug([self.viewItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]);
|
||||
|
||||
self.backgroundColor = [Theme backgroundColor];
|
||||
|
||||
[self configureFonts];
|
||||
|
||||
self.titleLabel.textColor = Theme.secondaryColor;
|
||||
for (UIButton *button in self.buttons) {
|
||||
[button setTitleColor:[UIColor ows_signalBlueColor] forState:UIControlStateNormal];
|
||||
[button setBackgroundColor:Theme.conversationButtonBackgroundColor];
|
||||
}
|
||||
|
||||
OWSContactOffersInteraction *interaction = (OWSContactOffersInteraction *)self.viewItem.interaction;
|
||||
|
||||
OWSAssertDebug(
|
||||
interaction.hasBlockOffer || interaction.hasAddToContactsOffer || interaction.hasAddToProfileWhitelistOffer);
|
||||
|
||||
self.addToContactsButton.hidden = !interaction.hasAddToContactsOffer;
|
||||
self.addToProfileWhitelistButton.hidden = !interaction.hasAddToProfileWhitelistOffer;
|
||||
self.blockButton.hidden = !interaction.hasBlockOffer;
|
||||
|
||||
[NSLayoutConstraint deactivateConstraints:self.layoutConstraints];
|
||||
self.layoutConstraints = @[
|
||||
[self.addToContactsButton autoSetDimension:ALDimensionHeight toSize:self.buttonHeight],
|
||||
[self.addToProfileWhitelistButton autoSetDimension:ALDimensionHeight toSize:self.buttonHeight],
|
||||
[self.blockButton autoSetDimension:ALDimensionHeight toSize:self.buttonHeight],
|
||||
|
||||
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.topVMargin],
|
||||
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.bottomVMargin],
|
||||
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeLeading
|
||||
withInset:self.conversationStyle.fullWidthGutterLeading],
|
||||
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTrailing
|
||||
withInset:self.conversationStyle.fullWidthGutterTrailing],
|
||||
];
|
||||
|
||||
// This hack fixes a bug that I don't understand.
|
||||
//
|
||||
// On an iPhone 5C running iOS 10.3.3,
|
||||
//
|
||||
// * Alice is a contact for which we should show some but not all contact offer buttons.
|
||||
// * Delete thread with Alice.
|
||||
// * Send yourself a message from Alice.
|
||||
// * Open conversation with Alice.
|
||||
//
|
||||
// Expected: Some (but not all) offer buttons are displayed.
|
||||
// Observed: All offer buttons are displayed, in a cramped layout.
|
||||
for (UIButton *button in self.buttons) {
|
||||
[button removeFromSuperview];
|
||||
}
|
||||
for (UIButton *button in self.buttons) {
|
||||
if (!button.hidden) {
|
||||
[self.buttonStackView addArrangedSubview:button];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<UIButton *> *)buttons
|
||||
{
|
||||
return @[
|
||||
self.addToContactsButton,
|
||||
self.addToProfileWhitelistButton,
|
||||
self.blockButton,
|
||||
];
|
||||
}
|
||||
|
||||
- (CGFloat)topVMargin
|
||||
{
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
- (CGFloat)bottomVMargin
|
||||
{
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
- (CGFloat)vSpacing
|
||||
{
|
||||
return 8.f;
|
||||
}
|
||||
|
||||
- (CGFloat)buttonHeight
|
||||
{
|
||||
return (24.f + self.addToContactsButton.titleLabel.font.lineHeight);
|
||||
}
|
||||
|
||||
- (CGSize)cellSize
|
||||
{
|
||||
OWSAssertDebug(self.conversationStyle);
|
||||
OWSAssertDebug(self.conversationStyle.viewWidth > 0);
|
||||
OWSAssertDebug(self.viewItem);
|
||||
OWSAssertDebug([self.viewItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]);
|
||||
|
||||
[self configureFonts];
|
||||
|
||||
OWSContactOffersInteraction *interaction = (OWSContactOffersInteraction *)self.viewItem.interaction;
|
||||
|
||||
CGSize result = CGSizeMake(self.conversationStyle.viewWidth, 0);
|
||||
result.height += self.topVMargin;
|
||||
result.height += self.bottomVMargin;
|
||||
|
||||
result.height += ceil([self.titleLabel sizeThatFits:CGSizeZero].height);
|
||||
|
||||
int buttonCount = ((interaction.hasBlockOffer ? 1 : 0) + (interaction.hasAddToContactsOffer ? 1 : 0)
|
||||
+ (interaction.hasAddToProfileWhitelistOffer ? 1 : 0));
|
||||
result.height += buttonCount * (self.vSpacing + self.buttonHeight);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#pragma mark - Events
|
||||
|
||||
- (nullable OWSContactOffersInteraction *)interaction
|
||||
{
|
||||
OWSAssertDebug(self.viewItem);
|
||||
OWSAssertDebug(self.viewItem.interaction);
|
||||
if (![self.viewItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]) {
|
||||
OWSFailDebug(@"expected OWSContactOffersInteraction but found: %@", self.viewItem.interaction);
|
||||
return nil;
|
||||
}
|
||||
return (OWSContactOffersInteraction *)self.viewItem.interaction;
|
||||
}
|
||||
|
||||
- (void)addToContacts
|
||||
{
|
||||
OWSAssertDebug(self.delegate);
|
||||
OWSAssertDebug(self.interaction);
|
||||
|
||||
[self.delegate tappedAddToContactsOfferMessage:self.interaction];
|
||||
}
|
||||
|
||||
- (void)addToProfileWhitelist
|
||||
{
|
||||
OWSAssertDebug(self.delegate);
|
||||
OWSAssertDebug(self.interaction);
|
||||
|
||||
[self.delegate tappedAddToProfileWhitelistOfferMessage:self.interaction];
|
||||
}
|
||||
|
||||
- (void)block
|
||||
{
|
||||
OWSAssertDebug(self.delegate);
|
||||
OWSAssertDebug(self.interaction);
|
||||
|
||||
[self.delegate tappedUnknownContactBlockOfferMessage:self.interaction];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,33 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ContactShareViewModel;
|
||||
|
||||
@protocol OWSContactShareButtonsViewDelegate <NSObject>
|
||||
|
||||
- (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare;
|
||||
- (void)didTapSendInviteToContactShare:(ContactShareViewModel *)contactShare;
|
||||
- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSContactShareButtonsView : UIView
|
||||
|
||||
- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare
|
||||
delegate:(id<OWSContactShareButtonsViewDelegate>)delegate;
|
||||
|
||||
+ (CGFloat)bubbleHeight;
|
||||
|
||||
// Returns YES IFF the tap was handled.
|
||||
- (BOOL)handleTapGesture:(UITapGestureRecognizer *)sender;
|
||||
|
||||
+ (BOOL)hasAnyButton:(ContactShareViewModel *)contactShare;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,169 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSContactShareButtonsView.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSContact.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSContactShareButtonsView ()
|
||||
|
||||
@property (nonatomic, readonly) ContactShareViewModel *contactShare;
|
||||
@property (nonatomic, weak) id<OWSContactShareButtonsViewDelegate> delegate;
|
||||
|
||||
@property (nonatomic, readonly) OWSContactsManager *contactsManager;
|
||||
|
||||
@property (nonatomic, nullable) UIView *buttonView;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSContactShareButtonsView
|
||||
|
||||
- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare
|
||||
delegate:(id<OWSContactShareButtonsViewDelegate>)delegate
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_delegate = delegate;
|
||||
_contactShare = contactShare;
|
||||
_contactsManager = Environment.shared.contactsManager;
|
||||
|
||||
[self createContents];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
+ (BOOL)hasSendTextButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager
|
||||
{
|
||||
OWSAssertDebug(contactShare);
|
||||
OWSAssertDebug(contactsManager);
|
||||
|
||||
return [contactShare systemContactsWithSignalAccountPhoneNumbers:contactsManager].count > 0;
|
||||
}
|
||||
|
||||
+ (BOOL)hasInviteButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager
|
||||
{
|
||||
OWSAssertDebug(contactShare);
|
||||
OWSAssertDebug(contactsManager);
|
||||
|
||||
return [contactShare systemContactPhoneNumbers:contactsManager].count > 0;
|
||||
}
|
||||
|
||||
+ (BOOL)hasAddToContactsButton:(ContactShareViewModel *)contactShare
|
||||
{
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
return [contactShare e164PhoneNumbers].count > 0;
|
||||
}
|
||||
|
||||
+ (BOOL)hasAnyButton:(ContactShareViewModel *)contactShare
|
||||
{
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
OWSContactsManager *contactsManager = Environment.shared.contactsManager;
|
||||
|
||||
return [self hasAnyButton:contactShare contactsManager:contactsManager];
|
||||
}
|
||||
|
||||
+ (BOOL)hasAnyButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager
|
||||
{
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
return ([self hasSendTextButton:contactShare contactsManager:contactsManager] ||
|
||||
[self hasInviteButton:contactShare contactsManager:contactsManager] ||
|
||||
[self hasAddToContactsButton:contactShare]);
|
||||
}
|
||||
|
||||
+ (CGFloat)bubbleHeight
|
||||
{
|
||||
return self.buttonHeight;
|
||||
}
|
||||
|
||||
+ (CGFloat)buttonHeight
|
||||
{
|
||||
return MAX(44.f, self.buttonFont.lineHeight + self.buttonVMargin * 2);
|
||||
}
|
||||
|
||||
+ (UIFont *)buttonFont
|
||||
{
|
||||
return [UIFont ows_dynamicTypeBodyFont].ows_mediumWeight;
|
||||
}
|
||||
|
||||
+ (CGFloat)buttonVMargin
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
|
||||
- (void)createContents
|
||||
{
|
||||
OWSAssertDebug([OWSContactShareButtonsView hasAnyButton:self.contactShare contactsManager:self.contactsManager]);
|
||||
|
||||
self.layoutMargins = UIEdgeInsetsZero;
|
||||
self.backgroundColor = Theme.conversationButtonBackgroundColor;
|
||||
|
||||
UILabel *label = [UILabel new];
|
||||
self.buttonView = label;
|
||||
if ([OWSContactShareButtonsView hasSendTextButton:self.contactShare contactsManager:self.contactsManager]) {
|
||||
label.text
|
||||
= NSLocalizedString(@"ACTION_SEND_MESSAGE", @"Label for button that lets you send a message to a contact.");
|
||||
} else if ([OWSContactShareButtonsView hasInviteButton:self.contactShare contactsManager:self.contactsManager]) {
|
||||
label.text = NSLocalizedString(@"ACTION_INVITE", @"Label for 'invite' button in contact view.");
|
||||
} else if ([OWSContactShareButtonsView hasAddToContactsButton:self.contactShare]) {
|
||||
label.text = NSLocalizedString(@"CONVERSATION_VIEW_ADD_TO_CONTACTS_OFFER",
|
||||
@"Message shown in conversation view that offers to add an unknown user to your phone's contacts.");
|
||||
} else {
|
||||
OWSFailDebug(@"unexpected button state.");
|
||||
}
|
||||
label.font = OWSContactShareButtonsView.buttonFont;
|
||||
label.textColor = (Theme.isDarkThemeEnabled ? UIColor.ows_whiteColor : UIColor.ows_materialBlueColor);
|
||||
label.textAlignment = NSTextAlignmentCenter;
|
||||
[self addSubview:label];
|
||||
[label ows_autoPinToSuperviewEdges];
|
||||
[label autoSetDimension:ALDimensionHeight toSize:OWSContactShareButtonsView.buttonHeight];
|
||||
|
||||
self.userInteractionEnabled = YES;
|
||||
UITapGestureRecognizer *tap =
|
||||
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
|
||||
[self addGestureRecognizer:tap];
|
||||
}
|
||||
|
||||
- (BOOL)handleTapGesture:(UITapGestureRecognizer *)sender
|
||||
{
|
||||
if (!self.buttonView) {
|
||||
return NO;
|
||||
}
|
||||
CGPoint location = [sender locationInView:self.buttonView];
|
||||
if (!CGRectContainsPoint(self.buttonView.bounds, location)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if ([OWSContactShareButtonsView hasSendTextButton:self.contactShare contactsManager:self.contactsManager]) {
|
||||
[self.delegate didTapSendMessageToContactShare:self.contactShare];
|
||||
} else if ([OWSContactShareButtonsView hasInviteButton:self.contactShare contactsManager:self.contactsManager]) {
|
||||
[self.delegate didTapSendInviteToContactShare:self.contactShare];
|
||||
} else if ([OWSContactShareButtonsView hasAddToContactsButton:self.contactShare]) {
|
||||
[self.delegate didTapShowAddToContactUIForContactShare:self.contactShare];
|
||||
} else {
|
||||
OWSFailDebug(@"unexpected button tap.");
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,22 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ContactShareViewModel;
|
||||
@class ConversationStyle;
|
||||
|
||||
@interface OWSContactShareView : UIView
|
||||
|
||||
- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare
|
||||
isIncoming:(BOOL)isIncoming
|
||||
conversationStyle:(ConversationStyle *)conversationStyle;
|
||||
|
||||
- (void)createContents;
|
||||
|
||||
+ (CGFloat)bubbleHeight;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,165 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSContactShareView.h"
|
||||
#import "OWSContactAvatarBuilder.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSContact.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSContactShareView ()
|
||||
|
||||
@property (nonatomic, readonly) ContactShareViewModel *contactShare;
|
||||
|
||||
@property (nonatomic, readonly) BOOL isIncoming;
|
||||
@property (nonatomic, readonly) ConversationStyle *conversationStyle;
|
||||
@property (nonatomic, readonly) OWSContactsManager *contactsManager;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSContactShareView
|
||||
|
||||
- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare
|
||||
isIncoming:(BOOL)isIncoming
|
||||
conversationStyle:(ConversationStyle *)conversationStyle
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_contactShare = contactShare;
|
||||
_isIncoming = isIncoming;
|
||||
_conversationStyle = conversationStyle;
|
||||
_contactsManager = Environment.shared.contactsManager;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (CGFloat)hMargin
|
||||
{
|
||||
return 12.f;
|
||||
}
|
||||
|
||||
+ (CGFloat)vMargin
|
||||
{
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
- (CGFloat)iconHSpacing
|
||||
{
|
||||
return 8.f;
|
||||
}
|
||||
|
||||
+ (CGFloat)bubbleHeight
|
||||
{
|
||||
return self.contentHeight;
|
||||
}
|
||||
|
||||
+ (CGFloat)contentHeight
|
||||
{
|
||||
CGFloat labelsHeight = (self.nameFont.lineHeight + self.labelsVSpacing + self.subtitleFont.lineHeight);
|
||||
CGFloat contentHeight = MAX(self.iconSize, labelsHeight);
|
||||
contentHeight += OWSContactShareView.vMargin * 2;
|
||||
return contentHeight;
|
||||
}
|
||||
|
||||
+ (CGFloat)iconSize
|
||||
{
|
||||
return kStandardAvatarSize;
|
||||
}
|
||||
|
||||
- (CGFloat)iconSize
|
||||
{
|
||||
return [OWSContactShareView iconSize];
|
||||
}
|
||||
|
||||
+ (UIFont *)nameFont
|
||||
{
|
||||
return [UIFont ows_dynamicTypeBodyFont];
|
||||
}
|
||||
|
||||
+ (UIFont *)subtitleFont
|
||||
{
|
||||
return [UIFont ows_dynamicTypeCaption1Font];
|
||||
}
|
||||
|
||||
+ (CGFloat)labelsVSpacing
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
- (void)createContents
|
||||
{
|
||||
self.layoutMargins = UIEdgeInsetsZero;
|
||||
|
||||
UIColor *textColor = [self.conversationStyle bubbleTextColorWithIsIncoming:self.isIncoming];
|
||||
|
||||
AvatarImageView *avatarView = [AvatarImageView new];
|
||||
avatarView.image =
|
||||
[self.contactShare getAvatarImageWithDiameter:self.iconSize contactsManager:self.contactsManager];
|
||||
|
||||
[avatarView autoSetDimension:ALDimensionWidth toSize:self.iconSize];
|
||||
[avatarView autoSetDimension:ALDimensionHeight toSize:self.iconSize];
|
||||
[avatarView setCompressionResistanceHigh];
|
||||
[avatarView setContentHuggingHigh];
|
||||
|
||||
UILabel *topLabel = [UILabel new];
|
||||
topLabel.text = self.contactShare.displayName;
|
||||
topLabel.textColor = textColor;
|
||||
topLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
topLabel.font = OWSContactShareView.nameFont;
|
||||
|
||||
UIStackView *labelsView = [UIStackView new];
|
||||
labelsView.axis = UILayoutConstraintAxisVertical;
|
||||
labelsView.spacing = OWSContactShareView.labelsVSpacing;
|
||||
[labelsView addArrangedSubview:topLabel];
|
||||
|
||||
NSString *_Nullable firstPhoneNumber =
|
||||
[self.contactShare systemContactsWithSignalAccountPhoneNumbers:self.contactsManager].firstObject;
|
||||
if (firstPhoneNumber.length > 0) {
|
||||
UILabel *bottomLabel = [UILabel new];
|
||||
bottomLabel.text = [PhoneNumber bestEffortLocalizedPhoneNumberWithE164:firstPhoneNumber];
|
||||
bottomLabel.textColor = [self.conversationStyle bubbleSecondaryTextColorWithIsIncoming:self.isIncoming];
|
||||
bottomLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
bottomLabel.font = OWSContactShareView.subtitleFont;
|
||||
[labelsView addArrangedSubview:bottomLabel];
|
||||
}
|
||||
|
||||
UIImage *disclosureImage =
|
||||
[UIImage imageNamed:(CurrentAppContext().isRTL ? @"small_chevron_left" : @"small_chevron_right")];
|
||||
OWSAssertDebug(disclosureImage);
|
||||
UIImageView *disclosureImageView = [UIImageView new];
|
||||
disclosureImageView.image = [disclosureImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
disclosureImageView.tintColor = textColor;
|
||||
[disclosureImageView setCompressionResistanceHigh];
|
||||
[disclosureImageView setContentHuggingHigh];
|
||||
|
||||
UIStackView *hStackView = [UIStackView new];
|
||||
hStackView.axis = UILayoutConstraintAxisHorizontal;
|
||||
hStackView.spacing = self.iconHSpacing;
|
||||
hStackView.alignment = UIStackViewAlignmentCenter;
|
||||
hStackView.layoutMarginsRelativeArrangement = YES;
|
||||
hStackView.layoutMargins
|
||||
= UIEdgeInsetsMake(OWSContactShareView.vMargin, self.hMargin, OWSContactShareView.vMargin, self.hMargin);
|
||||
[hStackView addArrangedSubview:avatarView];
|
||||
[hStackView addArrangedSubview:labelsView];
|
||||
[hStackView addArrangedSubview:disclosureImageView];
|
||||
[self addSubview:hStackView];
|
||||
[hStackView ows_autoPinToSuperviewEdges];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -7,12 +7,12 @@
|
|||
#import "Session-Swift.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
|
||||
#import <SignalUtilitiesKit/OWSFormat.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/MimeTypeUtil.h>
|
||||
#import <SignalUtilitiesKit/NSString+SSK.h>
|
||||
#import <SignalUtilitiesKit/TSAttachmentStream.h>
|
||||
#import <SessionUtilitiesKit/MIMETypeUtil.h>
|
||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||
#import <SessionMessagingKit/TSAttachmentStream.h>
|
||||
#import <SignalCoreKit/NSString+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
@ -91,7 +91,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (CGFloat)iconHeight
|
||||
{
|
||||
return kStandardAvatarSize;
|
||||
return 48.0f;
|
||||
}
|
||||
|
||||
- (void)createContentsWithConversationStyle:(ConversationStyle *)conversationStyle
|
||||
|
|
|
@ -49,15 +49,6 @@ typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) {
|
|||
|
||||
- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem linkPreview:(OWSLinkPreview *)linkPreview;
|
||||
|
||||
- (void)didTapContactShareViewItem:(id<ConversationViewItem>)viewItem;
|
||||
|
||||
- (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare
|
||||
NS_SWIFT_NAME(didTapSendMessage(toContactShare:));
|
||||
- (void)didTapSendInviteToContactShare:(ContactShareViewModel *)contactShare
|
||||
NS_SWIFT_NAME(didTapSendInvite(toContactShare:));
|
||||
- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare
|
||||
NS_SWIFT_NAME(didTapShowAddToContactUI(forContactShare:));
|
||||
|
||||
@property (nonatomic, readonly, nullable) NSString *lastSearchedText;
|
||||
|
||||
@end
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
#import "ConversationViewItem.h"
|
||||
#import "OWSBubbleShapeView.h"
|
||||
#import "OWSBubbleView.h"
|
||||
#import "OWSContactShareButtonsView.h"
|
||||
#import "OWSContactShareView.h"
|
||||
#import "OWSGenericAttachmentView.h"
|
||||
#import "OWSLabel.h"
|
||||
#import "OWSMessageFooterView.h"
|
||||
|
@ -16,11 +14,11 @@
|
|||
#import "OWSQuotedMessageView.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSMessageBubbleView () <OWSQuotedMessageViewDelegate, OWSContactShareButtonsViewDelegate>
|
||||
@interface OWSMessageBubbleView () <OWSQuotedMessageViewDelegate>
|
||||
|
||||
@property (nonatomic) OWSBubbleView *bubbleView;
|
||||
|
||||
|
@ -48,21 +46,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
@property (nonatomic) OWSMessageFooterView *footerView;
|
||||
|
||||
@property (nonatomic, nullable) OWSContactShareButtonsView *contactShareButtonsView;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSMessageBubbleView
|
||||
|
||||
#pragma mark - Dependencies
|
||||
|
||||
- (OWSAttachmentDownloads *)attachmentDownloads
|
||||
{
|
||||
return SSKEnvironment.shared.attachmentDownloads;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
|
@ -267,9 +256,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
case OWSMessageCellType_GenericAttachment:
|
||||
bodyMediaView = [self loadViewForGenericAttachment];
|
||||
break;
|
||||
case OWSMessageCellType_ContactShare:
|
||||
bodyMediaView = [self loadViewForContactShare];
|
||||
break;
|
||||
case OWSMessageCellType_MediaMessage:
|
||||
bodyMediaView = [self loadViewForMediaAlbum];
|
||||
break;
|
||||
|
@ -300,24 +286,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
if (self.hasBodyMediaWithThumbnail) {
|
||||
[self.stackView addArrangedSubview:bodyMediaView];
|
||||
} else {
|
||||
OWSAssertDebug(self.cellType == OWSMessageCellType_ContactShare);
|
||||
|
||||
if (self.contactShareHasSpacerTop) {
|
||||
UIView *spacerView = [UIView containerView];
|
||||
[spacerView autoSetDimension:ALDimensionHeight toSize:self.contactShareVSpacing];
|
||||
[spacerView setCompressionResistanceHigh];
|
||||
[self.stackView addArrangedSubview:spacerView];
|
||||
}
|
||||
|
||||
[self.stackView addArrangedSubview:bodyMediaView];
|
||||
|
||||
if (self.contactShareHasSpacerBottom) {
|
||||
UIView *spacerView = [UIView containerView];
|
||||
[spacerView autoSetDimension:ALDimensionHeight toSize:self.contactShareVSpacing];
|
||||
[spacerView setCompressionResistanceHigh];
|
||||
[self.stackView addArrangedSubview:spacerView];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
[textViews addObject:bodyMediaView];
|
||||
|
@ -414,86 +382,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
addObject:[bodyMediaView autoSetDimension:ALDimensionHeight toSize:bodyMediaSize.CGSizeValue.height]];
|
||||
}
|
||||
|
||||
[self insertContactShareButtonsIfNecessary];
|
||||
|
||||
[self updateBubbleColor];
|
||||
|
||||
[self configureBubbleRounding];
|
||||
}
|
||||
|
||||
- (void)insertContactShareButtonsIfNecessary
|
||||
{
|
||||
if (self.cellType != OWSMessageCellType_ContactShare) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (![OWSContactShareButtonsView hasAnyButton:self.viewItem.contactShare]) {
|
||||
return;
|
||||
}
|
||||
|
||||
OWSAssertDebug(self.viewItem.contactShare);
|
||||
|
||||
OWSContactShareButtonsView *buttonsView =
|
||||
[[OWSContactShareButtonsView alloc] initWithContactShare:self.viewItem.contactShare delegate:self];
|
||||
|
||||
NSValue *_Nullable actionButtonsSize = [self actionButtonsSize];
|
||||
OWSAssertDebug(actionButtonsSize);
|
||||
[self.viewConstraints addObjectsFromArray:@[
|
||||
[buttonsView autoSetDimension:ALDimensionHeight toSize:actionButtonsSize.CGSizeValue.height],
|
||||
]];
|
||||
|
||||
// The "contact share" view casts a shadow "downward" onto adjacent views,
|
||||
// so we use a "proxy" view to take its place within the v-stack
|
||||
// view and then insert the "contact share" view above its proxy so that
|
||||
// it floats above the other content of the bubble view.
|
||||
|
||||
UIView *proxyView = [UIView new];
|
||||
[self.stackView addArrangedSubview:proxyView];
|
||||
|
||||
OWSBubbleShapeView *shadowView = [[OWSBubbleShapeView alloc] initShadow];
|
||||
OWSBubbleShapeView *clipView = [[OWSBubbleShapeView alloc] initClip];
|
||||
|
||||
[self addSubview:shadowView];
|
||||
[self addSubview:clipView];
|
||||
|
||||
[self.viewConstraints addObjectsFromArray:[shadowView autoPinToEdgesOfView:proxyView]];
|
||||
[self.viewConstraints addObjectsFromArray:[clipView autoPinToEdgesOfView:proxyView]];
|
||||
|
||||
[clipView addSubview:buttonsView];
|
||||
[self.viewConstraints addObjectsFromArray:[buttonsView ows_autoPinToSuperviewEdges]];
|
||||
|
||||
[self.bubbleView addPartnerView:shadowView];
|
||||
[self.bubbleView addPartnerView:clipView];
|
||||
|
||||
// Prevent the layer from animating changes.
|
||||
[CATransaction begin];
|
||||
[CATransaction setDisableActions:YES];
|
||||
|
||||
OWSAssertDebug(buttonsView.backgroundColor);
|
||||
shadowView.fillColor = buttonsView.backgroundColor;
|
||||
shadowView.layer.shadowColor = Theme.boldColor.CGColor;
|
||||
shadowView.layer.shadowOpacity = 0.12f;
|
||||
shadowView.layer.shadowOffset = CGSizeZero;
|
||||
shadowView.layer.shadowRadius = 1.f;
|
||||
|
||||
[CATransaction commit];
|
||||
}
|
||||
|
||||
- (BOOL)contactShareHasSpacerTop
|
||||
{
|
||||
return (self.cellType == OWSMessageCellType_ContactShare && (self.isQuotedReply || !self.shouldShowSenderName));
|
||||
}
|
||||
|
||||
- (BOOL)contactShareHasSpacerBottom
|
||||
{
|
||||
return (self.cellType == OWSMessageCellType_ContactShare && !self.hasBottomFooter);
|
||||
}
|
||||
|
||||
- (CGFloat)contactShareVSpacing
|
||||
{
|
||||
return 12.f;
|
||||
}
|
||||
|
||||
- (CGFloat)senderNameBottomSpacing
|
||||
{
|
||||
return 0.f;
|
||||
|
@ -557,7 +450,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
case OWSMessageCellType_OversizeTextDownloading:
|
||||
return NO;
|
||||
case OWSMessageCellType_MediaMessage:
|
||||
|
@ -572,7 +464,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return NO;
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
case OWSMessageCellType_MediaMessage:
|
||||
case OWSMessageCellType_OversizeTextDownloading:
|
||||
return YES;
|
||||
|
@ -581,8 +472,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (BOOL)hasFullWidthMediaView
|
||||
{
|
||||
return (self.hasBodyMediaWithThumbnail || self.cellType == OWSMessageCellType_ContactShare
|
||||
|| self.cellType == OWSMessageCellType_MediaMessage);
|
||||
return (self.hasBodyMediaWithThumbnail || self.cellType == OWSMessageCellType_MediaMessage);
|
||||
}
|
||||
|
||||
- (BOOL)canFooterOverlayMedia
|
||||
|
@ -876,26 +766,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return attachmentView;
|
||||
}
|
||||
|
||||
- (UIView *)loadViewForContactShare
|
||||
{
|
||||
OWSAssertDebug(self.viewItem.contactShare);
|
||||
|
||||
OWSContactShareView *contactShareView = [[OWSContactShareView alloc] initWithContactShare:self.viewItem.contactShare
|
||||
isIncoming:self.isIncoming
|
||||
conversationStyle:self.conversationStyle];
|
||||
[contactShareView createContents];
|
||||
// TODO: Should we change appearance if contact avatar is uploading?
|
||||
|
||||
self.loadCellContentBlock = ^{
|
||||
// Do nothing.
|
||||
};
|
||||
self.unloadCellContentBlock = ^{
|
||||
// Do nothing.
|
||||
};
|
||||
|
||||
return contactShareView;
|
||||
}
|
||||
|
||||
- (UIView *)loadViewForOversizeTextDownload
|
||||
{
|
||||
// We can use an empty view. The progress views will display download
|
||||
|
@ -969,10 +839,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSFailDebug(@"Missing uniqueId.");
|
||||
return;
|
||||
}
|
||||
if ([self.attachmentDownloads downloadProgressForAttachmentId:uniqueId] == nil) {
|
||||
OWSFailDebug(@"Missing download progress.");
|
||||
return;
|
||||
}
|
||||
|
||||
UIView *overlayView = [UIView new];
|
||||
overlayView.backgroundColor = [self.bubbleColor colorWithAlphaComponent:0.5];
|
||||
|
@ -1079,11 +945,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
result = [attachmentView measureSizeWithMaxMessageWidth:maxMessageWidth];
|
||||
break;
|
||||
}
|
||||
case OWSMessageCellType_ContactShare:
|
||||
OWSAssertDebug(self.viewItem.contactShare);
|
||||
|
||||
result = CGSizeMake(maxMessageWidth, [OWSContactShareView bubbleHeight]);
|
||||
break;
|
||||
case OWSMessageCellType_MediaMessage:
|
||||
result = [OWSMediaAlbumCellView layoutSizeForMaxMessageWidth:maxMessageWidth
|
||||
items:self.viewItem.mediaAlbumItems];
|
||||
|
@ -1186,23 +1047,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return [NSValue valueWithCGSize:result];
|
||||
}
|
||||
|
||||
- (nullable NSValue *)actionButtonsSize
|
||||
{
|
||||
OWSAssertDebug(self.conversationStyle);
|
||||
OWSAssertDebug(self.conversationStyle.maxMessageWidth > 0);
|
||||
|
||||
if (self.cellType == OWSMessageCellType_ContactShare) {
|
||||
OWSAssertDebug(self.viewItem.contactShare);
|
||||
|
||||
if ([OWSContactShareButtonsView hasAnyButton:self.viewItem.contactShare]) {
|
||||
CGSize buttonsSize = CGSizeCeil(
|
||||
CGSizeMake(self.conversationStyle.maxMessageWidth, [OWSContactShareButtonsView bubbleHeight]));
|
||||
return [NSValue valueWithCGSize:buttonsSize];
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (CGSize)measureSize
|
||||
{
|
||||
OWSAssertDebug(self.conversationStyle);
|
||||
|
@ -1239,13 +1083,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[textViewSizes addObject:bodyMediaSize];
|
||||
bodyMediaSize = nil;
|
||||
}
|
||||
|
||||
if (self.contactShareHasSpacerTop) {
|
||||
cellSize.height += self.contactShareVSpacing;
|
||||
}
|
||||
if (self.contactShareHasSpacerBottom) {
|
||||
cellSize.height += self.contactShareVSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
if (bodyMediaSize || quotedMessageSize) {
|
||||
|
@ -1296,12 +1133,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
cellSize.height += self.tapForMoreHeight + self.textViewVSpacing;
|
||||
}
|
||||
|
||||
NSValue *_Nullable actionButtonsSize = [self actionButtonsSize];
|
||||
if (actionButtonsSize) {
|
||||
cellSize.width = MAX(cellSize.width, actionButtonsSize.CGSizeValue.width);
|
||||
cellSize.height += actionButtonsSize.CGSizeValue.height;
|
||||
}
|
||||
|
||||
cellSize = CGSizeCeil(cellSize);
|
||||
|
||||
OWSAssertDebug(cellSize.width <= self.conversationStyle.maxMessageWidth);
|
||||
|
@ -1395,9 +1226,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
}
|
||||
|
||||
[self.contactShareButtonsView removeFromSuperview];
|
||||
self.contactShareButtonsView = nil;
|
||||
|
||||
[self.linkPreviewView removeFromSuperview];
|
||||
self.linkPreviewView.state = nil;
|
||||
}
|
||||
|
@ -1430,12 +1258,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
}
|
||||
|
||||
if (self.contactShareButtonsView) {
|
||||
if ([self.contactShareButtonsView handleTapGesture:sender]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CGPoint locationInMessageBubble = [sender locationInView:self];
|
||||
switch ([self gestureLocationForLocation:locationInMessageBubble]) {
|
||||
case OWSMessageGestureLocation_Default:
|
||||
|
@ -1488,9 +1310,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[AttachmentSharing showShareUIForAttachment:self.viewItem.attachmentStream];
|
||||
}
|
||||
break;
|
||||
case OWSMessageCellType_ContactShare:
|
||||
[self.delegate didTapContactShareViewItem:self.viewItem];
|
||||
break;
|
||||
case OWSMessageCellType_MediaMessage: {
|
||||
OWSAssertDebug(self.bodyMediaView);
|
||||
OWSAssertDebug(self.viewItem.mediaAlbumItems.count > 0);
|
||||
|
@ -1603,32 +1422,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSFailDebug(@"Sent quoted replies should not be cancellable.");
|
||||
}
|
||||
|
||||
#pragma mark - OWSContactShareButtonsViewDelegate
|
||||
|
||||
- (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
[self.delegate didTapSendMessageToContactShare:contactShare];
|
||||
}
|
||||
|
||||
- (void)didTapSendInviteToContactShare:(ContactShareViewModel *)contactShare
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
[self.delegate didTapSendInviteToContactShare:contactShare];
|
||||
}
|
||||
|
||||
- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
[self.delegate didTapShowAddToContactUIForContactShare:contactShare];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
//
|
||||
|
||||
#import "OWSMessageCell.h"
|
||||
#import "OWSContactAvatarBuilder.h"
|
||||
#import "OWSMessageBubbleView.h"
|
||||
#import "OWSMessageHeaderView.h"
|
||||
#import "Session-Swift.h"
|
||||
|
@ -286,11 +285,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[self.avatarView update];
|
||||
|
||||
// Loki: Show the moderator icon if needed
|
||||
if (self.viewItem.isGroupThread && !self.viewItem.isRSSFeed) { // FIXME: This logic also shouldn't apply to closed groups
|
||||
__block SNOpenGroup *publicChat;
|
||||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:self.viewItem.interaction.uniqueThreadId transaction: transaction];
|
||||
}];
|
||||
if (self.viewItem.isGroupThread) { // FIXME: This logic also shouldn't apply to closed groups
|
||||
SNOpenGroup *publicChat = [LKStorage.shared getOpenGroupForThreadID:self.viewItem.interaction.uniqueThreadId];
|
||||
if (publicChat != nil) {
|
||||
BOOL isModerator = [SNOpenGroupAPI isUserModerator:incomingMessage.authorId forChannel:publicChat.channel onServer:publicChat.server];
|
||||
UIImage *moderatorIcon = [UIImage imageNamed:@"Crown"];
|
||||
|
|
|
@ -122,7 +122,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
TSContactThread *thread = [outgoingMessage.thread as:TSContactThread.class];
|
||||
if (thread != nil) {
|
||||
isNoteToSelf = [LKDatabaseUtilities isUserLinkedDevice:thread.contactIdentifier in:transaction];
|
||||
NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
|
||||
isNoteToSelf = ([thread.contactIdentifier isEqual:userPublicKey]);
|
||||
}
|
||||
}];
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
#import <SignalUtilitiesKit/OWSUnreadIndicator.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIFont+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
//
|
||||
|
||||
#import "OWSMessageTextView.h"
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
#import "UIView+OWS.h"
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalUtilitiesKit/NSTimer+OWS.h>
|
||||
#import <SessionUtilitiesKit/NSTimer+Proxying.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
#import "OWSBubbleView.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalCoreKit/NSString+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SignalUtilitiesKit/TSAttachmentStream.h>
|
||||
#import <SignalUtilitiesKit/TSMessage.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionMessagingKit/TSAttachmentStream.h>
|
||||
#import <SessionMessagingKit/TSMessage.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
@ -543,23 +543,15 @@ const CGFloat kRemotelySourcedContentRowSpacing = 4;
|
|||
NSString *_Nullable localNumber = [TSAccountManager localNumber];
|
||||
NSString *quotedAuthorText;
|
||||
if ([localNumber isEqualToString:self.quotedMessage.authorId]) {
|
||||
|
||||
if (self.isOutgoing) {
|
||||
quotedAuthorText = NSLocalizedString(@"You", @"");
|
||||
} else {
|
||||
quotedAuthorText = NSLocalizedString(@"You", @"");
|
||||
}
|
||||
quotedAuthorText = NSLocalizedString(@"You", @"");
|
||||
} else {
|
||||
OWSContactsManager *contactsManager = Environment.shared.contactsManager;
|
||||
__block NSString *quotedAuthor = [SSKEnvironment.shared.profileManager profileNameForRecipientWithID:self.quotedMessage.authorId] ?: [contactsManager contactOrProfileNameForPhoneIdentifier:self.quotedMessage.authorId];
|
||||
__block NSString *quotedAuthor = [SSKEnvironment.shared.profileManager profileNameForRecipientWithID:self.quotedMessage.authorId] ?: self.quotedMessage.authorId;
|
||||
|
||||
if (quotedAuthor == self.quotedMessage.authorId) {
|
||||
SNOpenGroup *openGroup = [LKStorage.shared getOpenGroupForThreadID:self.quotedMessage.threadId];
|
||||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
SNOpenGroup *publicChat = [LKDatabaseUtilities getPublicChatForThreadID:self.quotedMessage.threadId transaction:transaction];
|
||||
if (publicChat != nil) {
|
||||
quotedAuthor = [LKUserDisplayNameUtilities getPublicChatDisplayNameFor:self.quotedMessage.authorId in:publicChat.channel on:publicChat.server using:transaction];
|
||||
} else {
|
||||
quotedAuthor = [LKUserDisplayNameUtilities getPrivateChatDisplayNameFor:self.quotedMessage.authorId];
|
||||
if (openGroup != nil) {
|
||||
quotedAuthor = [LKUserDisplayNameUtilities getPublicChatDisplayNameFor:self.quotedMessage.authorId in:openGroup.channel on:openGroup.server using:transaction];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -9,12 +9,10 @@
|
|||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/OWSVerificationStateChangeMessage.h>
|
||||
#import <SignalUtilitiesKit/TSCall.h>
|
||||
#import <SignalUtilitiesKit/TSErrorMessage.h>
|
||||
#import <SignalUtilitiesKit/TSInfoMessage.h>
|
||||
#import <SessionMessagingKit/OWSDisappearingConfigurationUpdateInfoMessage.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SessionMessagingKit/TSErrorMessage.h>
|
||||
#import <SessionMessagingKit/TSInfoMessage.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
@ -290,20 +288,7 @@ typedef void (^SystemMessageActionBlock)(void);
|
|||
: [UIImage imageNamed:@"system_message_disappearing_messages_disabled"]);
|
||||
break;
|
||||
}
|
||||
case TSInfoMessageVerificationStateChange:
|
||||
OWSAssertDebug([interaction isKindOfClass:[OWSVerificationStateChangeMessage class]]);
|
||||
if ([interaction isKindOfClass:[OWSVerificationStateChangeMessage class]]) {
|
||||
OWSVerificationStateChangeMessage *message = (OWSVerificationStateChangeMessage *)interaction;
|
||||
BOOL isVerified = message.verificationState == OWSVerificationStateVerified;
|
||||
if (!isVerified) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
result = [UIImage imageNamed:@"system_message_verified"];
|
||||
break;
|
||||
}
|
||||
} else if ([interaction isKindOfClass:[TSCall class]]) {
|
||||
result = [UIImage imageNamed:@"system_message_call"];
|
||||
} else {
|
||||
OWSFailDebug(@"Unknown interaction type: %@", [interaction class]);
|
||||
return nil;
|
||||
|
@ -398,8 +383,6 @@ typedef void (^SystemMessageActionBlock)(void);
|
|||
return [self actionForErrorMessage:(TSErrorMessage *)interaction];
|
||||
} else if ([interaction isKindOfClass:[TSInfoMessage class]]) {
|
||||
return [self actionForInfoMessage:(TSInfoMessage *)interaction];
|
||||
} else if ([interaction isKindOfClass:[TSCall class]]) {
|
||||
return [self actionForCall:(TSCall *)interaction];
|
||||
} else {
|
||||
OWSFailDebug(@"Tap for system messages of unknown type: %@", [interaction class]);
|
||||
return nil;
|
||||
|
@ -414,21 +397,6 @@ typedef void (^SystemMessageActionBlock)(void);
|
|||
switch (message.errorType) {
|
||||
case TSErrorMessageInvalidKeyException:
|
||||
return nil;
|
||||
case TSErrorMessageNonBlockingIdentityChange:
|
||||
return [SystemMessageAction
|
||||
actionWithTitle:NSLocalizedString(@"SYSTEM_MESSAGE_ACTION_VERIFY_SAFETY_NUMBER",
|
||||
@"Label for button to verify a user's safety number.")
|
||||
block:^{
|
||||
[weakSelf.delegate tappedNonBlockingIdentityChangeForRecipientId:message.recipientId];
|
||||
}];
|
||||
case TSErrorMessageWrongTrustedIdentityKey:
|
||||
return [SystemMessageAction
|
||||
actionWithTitle:NSLocalizedString(@"SYSTEM_MESSAGE_ACTION_VERIFY_SAFETY_NUMBER",
|
||||
@"Label for button to verify a user's safety number.")
|
||||
block:^{
|
||||
[weakSelf.delegate
|
||||
tappedInvalidIdentityKeyErrorMessage:(TSInvalidIdentityKeyErrorMessage *)message];
|
||||
}];
|
||||
case TSErrorMessageMissingKeyId:
|
||||
case TSErrorMessageNoSession:
|
||||
case TSErrorMessageInvalidMessage:
|
||||
|
@ -486,48 +454,12 @@ typedef void (^SystemMessageActionBlock)(void);
|
|||
block:^{
|
||||
[weakSelf.delegate showConversationSettings];
|
||||
}];
|
||||
case TSInfoMessageVerificationStateChange:
|
||||
return [SystemMessageAction
|
||||
actionWithTitle:NSLocalizedString(@"SHOW_SAFETY_NUMBER_ACTION", @"Action sheet item")
|
||||
block:^{
|
||||
[weakSelf.delegate
|
||||
showFingerprintWithRecipientId:((OWSVerificationStateChangeMessage *)message)
|
||||
.recipientId];
|
||||
}];
|
||||
}
|
||||
|
||||
OWSLogInfo(@"Unhandled tap for info message: %@", message);
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (nullable SystemMessageAction *)actionForCall:(TSCall *)call
|
||||
{
|
||||
OWSAssertDebug(call);
|
||||
|
||||
__weak OWSSystemMessageCell *weakSelf = self;
|
||||
switch (call.callType) {
|
||||
case RPRecentCallTypeIncoming:
|
||||
case RPRecentCallTypeIncomingMissed:
|
||||
case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity:
|
||||
case RPRecentCallTypeIncomingDeclined:
|
||||
return
|
||||
[SystemMessageAction actionWithTitle:NSLocalizedString(@"CALLBACK_BUTTON_TITLE", @"notification action")
|
||||
block:^{
|
||||
[weakSelf.delegate handleCallTap:call];
|
||||
}];
|
||||
case RPRecentCallTypeOutgoing:
|
||||
case RPRecentCallTypeOutgoingMissed:
|
||||
return [SystemMessageAction actionWithTitle:NSLocalizedString(@"CALL_AGAIN_BUTTON_TITLE",
|
||||
@"Label for button that lets users call a contact again.")
|
||||
block:^{
|
||||
[weakSelf.delegate handleCallTap:call];
|
||||
}];
|
||||
case RPRecentCallTypeOutgoingIncomplete:
|
||||
case RPRecentCallTypeIncomingIncomplete:
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Events
|
||||
|
||||
- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)longPress
|
||||
|
|
|
@ -19,7 +19,7 @@ public class TypingIndicatorCell: ConversationViewCell {
|
|||
private let kAvatarSize: CGFloat = 36
|
||||
private let kAvatarHSpacing: CGFloat = 8
|
||||
|
||||
private let avatarView = AvatarImageView()
|
||||
// private let avatarView = AvatarImageView()
|
||||
private let bubbleView = OWSBubbleView()
|
||||
private let typingIndicatorView = TypingIndicatorView()
|
||||
private var viewConstraints = [NSLayoutConstraint]()
|
||||
|
@ -39,8 +39,8 @@ public class TypingIndicatorCell: ConversationViewCell {
|
|||
bubbleView.addSubview(typingIndicatorView)
|
||||
contentView.addSubview(bubbleView)
|
||||
|
||||
avatarView.autoSetDimension(.width, toSize: kAvatarSize)
|
||||
avatarView.autoSetDimension(.height, toSize: kAvatarSize)
|
||||
// avatarView.autoSetDimension(.width, toSize: kAvatarSize)
|
||||
// avatarView.autoSetDimension(.height, toSize: kAvatarSize)
|
||||
}
|
||||
|
||||
@objc
|
||||
|
@ -65,16 +65,16 @@ public class TypingIndicatorCell: ConversationViewCell {
|
|||
typingIndicatorView.autoPinBottomToSuperviewMargin(withInset: conversationStyle.textInsetBottom)
|
||||
])
|
||||
|
||||
if let avatarView = configureAvatarView() {
|
||||
contentView.addSubview(avatarView)
|
||||
viewConstraints.append(contentsOf: [
|
||||
bubbleView.autoPinLeading(toTrailingEdgeOf: avatarView, offset: kAvatarHSpacing),
|
||||
bubbleView.autoAlignAxis(.horizontal, toSameAxisOf: avatarView)
|
||||
])
|
||||
|
||||
} else {
|
||||
avatarView.removeFromSuperview()
|
||||
}
|
||||
// if let avatarView = configureAvatarView() {
|
||||
// contentView.addSubview(avatarView)
|
||||
// viewConstraints.append(contentsOf: [
|
||||
// bubbleView.autoPinLeading(toTrailingEdgeOf: avatarView, offset: kAvatarHSpacing),
|
||||
// bubbleView.autoAlignAxis(.horizontal, toSameAxisOf: avatarView)
|
||||
// ])
|
||||
//
|
||||
// } else {
|
||||
// avatarView.removeFromSuperview()
|
||||
// }
|
||||
}
|
||||
|
||||
private func configureAvatarView() -> UIView? {
|
||||
|
@ -93,15 +93,16 @@ public class TypingIndicatorCell: ConversationViewCell {
|
|||
owsFailDebug("Missing authorConversationColorName")
|
||||
return nil
|
||||
}
|
||||
guard let authorAvatarImage =
|
||||
OWSContactAvatarBuilder(signalId: typingIndicators.recipientId,
|
||||
colorName: ConversationColorName(rawValue: colorName),
|
||||
diameter: UInt(kAvatarSize)).build() else {
|
||||
owsFailDebug("Could build avatar image")
|
||||
return nil
|
||||
}
|
||||
avatarView.image = authorAvatarImage
|
||||
return avatarView
|
||||
// guard let authorAvatarImage =
|
||||
// OWSContactAvatarBuilder(signalId: typingIndicators.recipientId,
|
||||
// colorName: ConversationColorName(rawValue: colorName),
|
||||
// diameter: UInt(kAvatarSize)).build() else {
|
||||
// owsFailDebug("Could build avatar image")
|
||||
// return nil
|
||||
// }
|
||||
// avatarView.image = authorAvatarImage
|
||||
// return avatarView
|
||||
return UIView()
|
||||
}
|
||||
|
||||
private func shouldShowAvatar() -> Bool {
|
||||
|
@ -140,8 +141,8 @@ public class TypingIndicatorCell: ConversationViewCell {
|
|||
NSLayoutConstraint.deactivate(viewConstraints)
|
||||
viewConstraints = [NSLayoutConstraint]()
|
||||
|
||||
avatarView.image = nil
|
||||
avatarView.removeFromSuperview()
|
||||
// avatarView.image = nil
|
||||
// avatarView.removeFromSuperview()
|
||||
|
||||
typingIndicatorView.stopAnimation()
|
||||
}
|
||||
|
|
|
@ -1,131 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc
|
||||
public protocol ConversationHeaderViewDelegate {
|
||||
func didTapConversationHeaderView(_ conversationHeaderView: ConversationHeaderView)
|
||||
}
|
||||
|
||||
@objc
|
||||
public class ConversationHeaderView: UIStackView {
|
||||
|
||||
@objc
|
||||
public weak var delegate: ConversationHeaderViewDelegate?
|
||||
|
||||
@objc
|
||||
public var attributedTitle: NSAttributedString? {
|
||||
get {
|
||||
return self.titleLabel.attributedText
|
||||
}
|
||||
set {
|
||||
self.titleLabel.attributedText = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public var attributedSubtitle: NSAttributedString? {
|
||||
get {
|
||||
return self.subtitleLabel.attributedText
|
||||
}
|
||||
set {
|
||||
self.subtitleLabel.attributedText = newValue
|
||||
self.subtitleLabel.isHidden = newValue == nil
|
||||
}
|
||||
}
|
||||
|
||||
public var avatarImage: UIImage? {
|
||||
get {
|
||||
return self.avatarView.image
|
||||
}
|
||||
set {
|
||||
self.avatarView.image = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public let titlePrimaryFont: UIFont = UIFont.ows_boldFont(withSize: 17)
|
||||
@objc
|
||||
public let titleSecondaryFont: UIFont = UIFont.ows_regularFont(withSize: 9)
|
||||
@objc
|
||||
public let subtitleFont: UIFont = UIFont.ows_regularFont(withSize: 12)
|
||||
|
||||
private let titleLabel: UILabel
|
||||
private let subtitleLabel: UILabel
|
||||
private let avatarView: ConversationAvatarImageView
|
||||
|
||||
@objc
|
||||
public required init(thread: TSThread, contactsManager: OWSContactsManager) {
|
||||
|
||||
let avatarView = ConversationAvatarImageView(thread: thread, diameter: 36, contactsManager: contactsManager)
|
||||
self.avatarView = avatarView
|
||||
|
||||
titleLabel = UILabel()
|
||||
titleLabel.textColor = Theme.navbarTitleColor
|
||||
titleLabel.lineBreakMode = .byTruncatingTail
|
||||
titleLabel.font = titlePrimaryFont
|
||||
titleLabel.setContentHuggingHigh()
|
||||
|
||||
subtitleLabel = UILabel()
|
||||
subtitleLabel.textColor = Theme.navbarTitleColor
|
||||
subtitleLabel.lineBreakMode = .byTruncatingTail
|
||||
subtitleLabel.font = subtitleFont
|
||||
subtitleLabel.setContentHuggingHigh()
|
||||
|
||||
let textRows = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
|
||||
textRows.axis = .vertical
|
||||
textRows.alignment = .leading
|
||||
textRows.distribution = .fillProportionally
|
||||
textRows.spacing = 0
|
||||
|
||||
textRows.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
|
||||
textRows.isLayoutMarginsRelativeArrangement = true
|
||||
|
||||
// low content hugging so that the text rows push container to the right bar button item(s)
|
||||
textRows.setContentHuggingLow()
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.layoutMargins = UIEdgeInsets(top: 4, left: 2, bottom: 4, right: 2)
|
||||
self.isLayoutMarginsRelativeArrangement = true
|
||||
|
||||
self.axis = .horizontal
|
||||
self.alignment = .center
|
||||
self.spacing = 0
|
||||
self.addArrangedSubview(avatarView)
|
||||
self.addArrangedSubview(textRows)
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapView))
|
||||
self.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
required public init(coder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
required public override init(frame: CGRect) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
public override var intrinsicContentSize: CGSize {
|
||||
// Grow to fill as much of the navbar as possible.
|
||||
return UIView.layoutFittingExpandedSize
|
||||
}
|
||||
|
||||
@objc
|
||||
public func updateAvatar() {
|
||||
self.avatarView.updateImage()
|
||||
}
|
||||
|
||||
// MARK: Delegate Methods
|
||||
|
||||
@objc func didTapView(tapGesture: UITapGestureRecognizer) {
|
||||
guard tapGesture.state == .recognized else {
|
||||
return
|
||||
}
|
||||
|
||||
self.delegate?.didTapConversationHeaderView(self)
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
#import "ConversationInputTextView.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalUtilitiesKit/NSString+SSK.h>
|
||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||
#import <SignalCoreKit/NSString+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
|
|
@ -5,18 +5,15 @@
|
|||
#import "ConversationInputToolbar.h"
|
||||
#import "ConversationInputTextView.h"
|
||||
#import "Environment.h"
|
||||
#import "OWSContactsManager.h"
|
||||
#import "OWSMath.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalUtilitiesKit/OWSFormat.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SignalUtilitiesKit/NSTimer+OWS.h>
|
||||
#import <SignalUtilitiesKit/TSQuotedMessage.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionMessagingKit/TSQuotedMessage.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
@ -1092,10 +1089,7 @@ const CGFloat kMaxTextViewHeight = 120;
|
|||
|
||||
- (void)showMentionCandidateSelectionViewFor:(NSArray<LKMention *> *)mentionCandidates in:(TSThread *)thread
|
||||
{
|
||||
__block SNOpenGroup *publicChat;
|
||||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:thread.uniqueId transaction:transaction];
|
||||
}];
|
||||
SNOpenGroup *publicChat = [LKStorage.shared getOpenGroupForThreadID:thread.uniqueId];
|
||||
if (publicChat != nil) {
|
||||
self.mentionCandidateSelectionView.publicChatServer = publicChat.server;
|
||||
[self.mentionCandidateSelectionView setPublicChatChannel:publicChat.channel];
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -3,7 +3,7 @@
|
|||
//
|
||||
|
||||
#import "ConversationViewLayout.h"
|
||||
#import "OWSAudioPlayer.h"
|
||||
#import <SessionMessagingKit/OWSAudioPlayer.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
@ -12,7 +12,6 @@ typedef NS_ENUM(NSInteger, OWSMessageCellType) {
|
|||
OWSMessageCellType_TextOnlyMessage,
|
||||
OWSMessageCellType_Audio,
|
||||
OWSMessageCellType_GenericAttachment,
|
||||
OWSMessageCellType_ContactShare,
|
||||
OWSMessageCellType_MediaMessage,
|
||||
OWSMessageCellType_OversizeTextDownloading,
|
||||
};
|
||||
|
@ -67,7 +66,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
|
|||
@property (nonatomic, readonly, nullable) OWSQuotedReplyModel *quotedReply;
|
||||
|
||||
@property (nonatomic, readonly) BOOL isGroupThread;
|
||||
@property (nonatomic, readonly) BOOL isRSSFeed;
|
||||
@property (nonatomic, readonly) BOOL userCanDeleteGroupMessage;
|
||||
|
||||
@property (nonatomic, readonly) BOOL hasBodyText;
|
||||
|
@ -163,7 +161,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
|
|||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithInteraction:(TSInteraction *)interaction
|
||||
isGroupThread:(BOOL)isGroupThread
|
||||
isRSSFeed:(BOOL)isRSSFeed
|
||||
transaction:(YapDatabaseReadTransaction *)transaction
|
||||
conversationStyle:(ConversationStyle *)conversationStyle;
|
||||
|
||||
|
|
|
@ -4,18 +4,18 @@
|
|||
|
||||
#import <CoreServices/CoreServices.h>
|
||||
#import "ConversationViewItem.h"
|
||||
#import "OWSContactOffersCell.h"
|
||||
|
||||
#import "OWSMessageCell.h"
|
||||
#import "OWSMessageHeaderView.h"
|
||||
#import "OWSSystemMessageCell.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "AnyPromise.h"
|
||||
#import <SignalUtilitiesKit/OWSUnreadIndicator.h>
|
||||
#import <SignalUtilitiesKit/NSData+Image.h>
|
||||
#import <SignalUtilitiesKit/NSString+SSK.h>
|
||||
#import <SignalUtilitiesKit/OWSContact.h>
|
||||
#import <SignalUtilitiesKit/TSInteraction.h>
|
||||
#import <SignalUtilitiesKit/SSKEnvironment.h>
|
||||
#import <SessionUtilitiesKit/NSData+Image.h>
|
||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||
|
||||
#import <SessionMessagingKit/TSInteraction.h>
|
||||
#import <SessionMessagingKit/SSKEnvironment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
@ -31,8 +31,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
return @"OWSMessageCellType_GenericAttachment";
|
||||
case OWSMessageCellType_Unknown:
|
||||
return @"OWSMessageCellType_Unknown";
|
||||
case OWSMessageCellType_ContactShare:
|
||||
return @"OWSMessageCellType_ContactShare";
|
||||
case OWSMessageCellType_MediaMessage:
|
||||
return @"OWSMessageCellType_MediaMessage";
|
||||
case OWSMessageCellType_OversizeTextDownloading:
|
||||
|
@ -119,7 +117,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
@synthesize interaction = _interaction;
|
||||
@synthesize isFirstInCluster = _isFirstInCluster;
|
||||
@synthesize isGroupThread = _isGroupThread;
|
||||
@synthesize isRSSFeed = _isRSSFeed;
|
||||
@synthesize isLastInCluster = _isLastInCluster;
|
||||
@synthesize lastAudioMessageView = _lastAudioMessageView;
|
||||
@synthesize senderName = _senderName;
|
||||
|
@ -127,7 +124,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
|
||||
- (instancetype)initWithInteraction:(TSInteraction *)interaction
|
||||
isGroupThread:(BOOL)isGroupThread
|
||||
isRSSFeed:(BOOL)isRSSFeed
|
||||
transaction:(YapDatabaseReadTransaction *)transaction
|
||||
conversationStyle:(ConversationStyle *)conversationStyle
|
||||
{
|
||||
|
@ -143,11 +139,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
|
||||
_interaction = interaction;
|
||||
_isGroupThread = isGroupThread;
|
||||
_isRSSFeed = isRSSFeed;
|
||||
_conversationStyle = conversationStyle;
|
||||
|
||||
[self updateAuthorConversationColorNameWithTransaction:transaction];
|
||||
|
||||
[self ensureViewState:transaction];
|
||||
|
||||
return self;
|
||||
|
@ -173,37 +166,11 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
self.linkPreview = nil;
|
||||
self.linkPreviewAttachment = nil;
|
||||
|
||||
[self updateAuthorConversationColorNameWithTransaction:transaction];
|
||||
|
||||
[self clearCachedLayoutState];
|
||||
|
||||
[self ensureViewState:transaction];
|
||||
}
|
||||
|
||||
- (void)updateAuthorConversationColorNameWithTransaction:(YapDatabaseReadTransaction *)transaction
|
||||
{
|
||||
OWSAssertDebug(transaction);
|
||||
|
||||
switch (self.interaction.interactionType) {
|
||||
case OWSInteractionType_TypingIndicator: {
|
||||
OWSTypingIndicatorInteraction *typingIndicator = (OWSTypingIndicatorInteraction *)self.interaction;
|
||||
_authorConversationColorName =
|
||||
[TSContactThread conversationColorNameForRecipientId:typingIndicator.recipientId
|
||||
transaction:transaction];
|
||||
break;
|
||||
}
|
||||
case OWSInteractionType_IncomingMessage: {
|
||||
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)self.interaction;
|
||||
_authorConversationColorName =
|
||||
[TSContactThread conversationColorNameForRecipientId:incomingMessage.authorId transaction:transaction];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
_authorConversationColorName = nil;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (OWSPrimaryStorage *)primaryStorage
|
||||
{
|
||||
return SSKEnvironment.shared.primaryStorage;
|
||||
|
@ -385,9 +352,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
case OWSInteractionType_Call:
|
||||
measurementCell = [OWSSystemMessageCell new];
|
||||
break;
|
||||
case OWSInteractionType_Offer:
|
||||
measurementCell = [OWSContactOffersCell new];
|
||||
break;
|
||||
case OWSInteractionType_TypingIndicator:
|
||||
measurementCell = [OWSTypingIndicatorCell new];
|
||||
break;
|
||||
|
@ -445,10 +409,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
case OWSInteractionType_Call:
|
||||
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSSystemMessageCell cellReuseIdentifier]
|
||||
forIndexPath:indexPath];
|
||||
case OWSInteractionType_Offer:
|
||||
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSContactOffersCell cellReuseIdentifier]
|
||||
forIndexPath:indexPath];
|
||||
|
||||
case OWSInteractionType_TypingIndicator:
|
||||
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSTypingIndicatorCell cellReuseIdentifier]
|
||||
forIndexPath:indexPath];
|
||||
|
@ -489,6 +449,15 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
[self.lastAudioMessageView setProgress:progress / duration];
|
||||
}
|
||||
|
||||
- (void)showInvalidAudioFileAlert
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[OWSAlerts
|
||||
showErrorAlertWithMessage:NSLocalizedString(@"INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE",
|
||||
@"Message for the alert indicating that an audio file is invalid.")];
|
||||
}
|
||||
|
||||
#pragma mark - Displayable Text
|
||||
|
||||
// TODO: Now that we're caching the displayable text on the view items,
|
||||
|
@ -609,12 +578,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
self.hasViewState = YES;
|
||||
|
||||
TSMessage *message = (TSMessage *)self.interaction;
|
||||
if (message.contactShare) {
|
||||
self.contactShare =
|
||||
[[ContactShareViewModel alloc] initWithContactShareRecord:message.contactShare transaction:transaction];
|
||||
self.messageCellType = OWSMessageCellType_ContactShare;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for quoted replies _before_ media album handling,
|
||||
// since that logic may exit early.
|
||||
|
@ -804,39 +767,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
}
|
||||
case OWSInteractionType_Info: {
|
||||
TSInfoMessage *infoMessage = (TSInfoMessage *)self.interaction;
|
||||
if ([infoMessage isKindOfClass:[OWSVerificationStateChangeMessage class]]) {
|
||||
OWSVerificationStateChangeMessage *verificationMessage
|
||||
= (OWSVerificationStateChangeMessage *)infoMessage;
|
||||
BOOL isVerified = verificationMessage.verificationState == OWSVerificationStateVerified;
|
||||
NSString *displayName =
|
||||
[Environment.shared.contactsManager displayNameForPhoneIdentifier:verificationMessage.recipientId];
|
||||
NSString *titleFormat = (isVerified
|
||||
? (verificationMessage.isLocalChange
|
||||
? NSLocalizedString(@"VERIFICATION_STATE_CHANGE_FORMAT_VERIFIED_LOCAL",
|
||||
@"Format for info message indicating that the verification state was verified "
|
||||
@"on "
|
||||
@"this device. Embeds {{user's name or phone number}}.")
|
||||
: NSLocalizedString(@"VERIFICATION_STATE_CHANGE_FORMAT_VERIFIED_OTHER_DEVICE",
|
||||
@"Format for info message indicating that the verification state was verified "
|
||||
@"on "
|
||||
@"another device. Embeds {{user's name or phone number}}."))
|
||||
: (verificationMessage.isLocalChange
|
||||
? NSLocalizedString(@"VERIFICATION_STATE_CHANGE_FORMAT_NOT_VERIFIED_LOCAL",
|
||||
@"Format for info message indicating that the verification state was "
|
||||
@"unverified on "
|
||||
@"this device. Embeds {{user's name or phone number}}.")
|
||||
: NSLocalizedString(@"VERIFICATION_STATE_CHANGE_FORMAT_NOT_VERIFIED_OTHER_DEVICE",
|
||||
@"Format for info message indicating that the verification state was "
|
||||
@"unverified on "
|
||||
@"another device. Embeds {{user's name or phone number}}.")));
|
||||
return [NSString stringWithFormat:titleFormat, displayName];
|
||||
} else {
|
||||
return [infoMessage previewTextWithTransaction:transaction];
|
||||
}
|
||||
}
|
||||
case OWSInteractionType_Call: {
|
||||
TSCall *call = (TSCall *)self.interaction;
|
||||
return [call previewTextWithTransaction:transaction];
|
||||
return [infoMessage previewTextWithTransaction:transaction];
|
||||
}
|
||||
default:
|
||||
OWSFailDebug(@"not a system message.");
|
||||
|
@ -921,11 +852,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
OWSFailDebug(@"No text to copy");
|
||||
break;
|
||||
}
|
||||
case OWSMessageCellType_ContactShare: {
|
||||
// TODO: Implement copy contact.
|
||||
OWSFailDebug(@"Not implemented yet");
|
||||
break;
|
||||
}
|
||||
case OWSMessageCellType_OversizeTextDownloading:
|
||||
OWSFailDebug(@"Can't copy not-yet-downloaded attachment");
|
||||
return;
|
||||
|
@ -942,10 +868,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare: {
|
||||
OWSFailDebug(@"No media to copy");
|
||||
break;
|
||||
}
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_GenericAttachment: {
|
||||
[self copyAttachmentToPasteboard:self.attachmentStream];
|
||||
|
@ -996,9 +918,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
OWSFailDebug(@"No media to share.");
|
||||
break;
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
[AttachmentSharing showShareUIForAttachment:self.attachmentStream];
|
||||
|
@ -1035,8 +954,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
return NO;
|
||||
case OWSMessageCellType_Audio:
|
||||
return NO;
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
|
@ -1064,8 +981,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
return NO;
|
||||
case OWSMessageCellType_Audio:
|
||||
return NO;
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
|
@ -1104,9 +1019,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
OWSFailDebug(@"Cannot save text data.");
|
||||
break;
|
||||
case OWSMessageCellType_Audio:
|
||||
OWSFailDebug(@"Cannot save media data.");
|
||||
break;
|
||||
|
@ -1172,30 +1084,37 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
|
||||
- (void)deleteAction
|
||||
{
|
||||
[self.interaction remove];
|
||||
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[self.interaction removeWithTransaction:transaction];
|
||||
if (self.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
|
||||
[LKStorage.shared cancelPendingMessageSendJobIfNeededForMessage:self.interaction.timestamp using:transaction];
|
||||
}
|
||||
}];
|
||||
|
||||
if (self.isGroupThread) {
|
||||
// Skip if the thread is an RSS feed
|
||||
TSGroupThread *groupThread = (TSGroupThread *)self.interaction.thread;
|
||||
if (groupThread.isRSSFeed) return;
|
||||
|
||||
// Only allow deletion on incoming and outgoing messages
|
||||
OWSInteractionType interationType = self.interaction.interactionType;
|
||||
if (interationType != OWSInteractionType_IncomingMessage && interationType != OWSInteractionType_OutgoingMessage) return;
|
||||
|
||||
// Make sure it's a public chat message
|
||||
// Make sure it's an open group message
|
||||
TSMessage *message = (TSMessage *)self.interaction;
|
||||
if (!message.isOpenGroupMessage) return;
|
||||
|
||||
__block SNOpenGroup *publicChat;
|
||||
[self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:groupThread.uniqueId transaction: transaction];
|
||||
}];
|
||||
if (publicChat == nil) return;
|
||||
|
||||
// Get the open group
|
||||
SNOpenGroup *openGroup = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId];
|
||||
if (openGroup == nil) return;
|
||||
|
||||
// If it's an incoming message the user must have moderator status
|
||||
if (self.interaction.interactionType == OWSInteractionType_IncomingMessage) {
|
||||
NSString *userPublicKey = [LKStorage.shared getUserPublicKey];
|
||||
if (![SNOpenGroupAPI isUserModerator:userPublicKey forChannel:openGroup.channel onServer:openGroup.server]) { return; }
|
||||
}
|
||||
|
||||
// Delete the message
|
||||
BOOL isSentByUser = (interationType == OWSInteractionType_OutgoingMessage);
|
||||
[[SNOpenGroupAPI deleteMessageWithID:message.openGroupServerMessageID forGroup:publicChat.channel onServer:publicChat.server isSentByUser:isSentByUser].catch(^(NSError *error) {
|
||||
BOOL wasSentByUser = (interationType == OWSInteractionType_OutgoingMessage);
|
||||
[[SNOpenGroupAPI deleteMessageWithID:message.openGroupServerMessageID forGroup:openGroup.channel onServer:openGroup.server isSentByUser:wasSentByUser].catch(^(NSError *error) {
|
||||
// Roll back
|
||||
[self.interaction save];
|
||||
}) retainUntilComplete];
|
||||
|
@ -1217,8 +1136,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
return NO;
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
return self.attachmentStream != nil;
|
||||
|
@ -1252,7 +1169,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
|
||||
// Ensure the thread is a public chat and not an RSS feed
|
||||
TSGroupThread *groupThread = (TSGroupThread *)self.interaction.thread;
|
||||
if (groupThread.isRSSFeed) return false;
|
||||
|
||||
// Only allow deletion on incoming and outgoing messages
|
||||
OWSInteractionType interationType = self.interaction.interactionType;
|
||||
|
@ -1263,18 +1179,14 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
if (!message.isOpenGroupMessage) return true;
|
||||
|
||||
// Ensure we have the details needed to contact the server
|
||||
__block SNOpenGroup *publicChat;
|
||||
[self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:groupThread.uniqueId transaction: transaction];
|
||||
}];
|
||||
SNOpenGroup *publicChat = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId];
|
||||
if (publicChat == nil) return true;
|
||||
|
||||
if (interationType == OWSInteractionType_IncomingMessage) {
|
||||
// Only allow deletion on incoming messages if the user has moderation permission
|
||||
return [SNOpenGroupAPI isUserModerator:self.userHexEncodedPublicKey forChannel:publicChat.channel onServer:publicChat.server];
|
||||
} else {
|
||||
// Only allow deletion on outgoing messages if the user was the sender (i.e. it was not sent from another linked device)
|
||||
return [self.interaction.actualSenderHexEncodedPublicKey isEqual:self.userHexEncodedPublicKey];
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -98,12 +98,10 @@ typedef NS_ENUM(NSUInteger, ConversationUpdateItemType) {
|
|||
@property (nonatomic, readonly) ConversationViewState *viewState;
|
||||
@property (nonatomic, nullable) NSString *focusMessageIdOnOpen;
|
||||
@property (nonatomic, readonly, nullable) ThreadDynamicInteractions *dynamicInteractions;
|
||||
@property (nonatomic, readonly) BOOL isRSSFeed;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithThread:(TSThread *)thread
|
||||
focusMessageIdOnOpen:(nullable NSString *)focusMessageIdOnOpen
|
||||
isRSSFeed:(BOOL)isRSSFeed
|
||||
delegate:(id<ConversationViewModelDelegate>)delegate NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (void)ensureDynamicInteractionsAndUpdateIfNecessary:(BOOL)updateIfNecessary;
|
||||
|
|
|
@ -9,20 +9,17 @@
|
|||
#import "OWSQuotedReplyModel.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSContactOffersInteraction.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/OWSUnreadIndicator.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/ThreadUtil.h>
|
||||
#import <SignalUtilitiesKit/OWSBlockingManager.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage.h>
|
||||
#import <SignalUtilitiesKit/SSKEnvironment.h>
|
||||
#import <SignalUtilitiesKit/TSDatabaseView.h>
|
||||
#import <SignalUtilitiesKit/TSIncomingMessage.h>
|
||||
#import <SignalUtilitiesKit/TSOutgoingMessage.h>
|
||||
#import <SignalUtilitiesKit/TSThread.h>
|
||||
#import <SignalUtilitiesKit/TSGroupThread.h>
|
||||
#import <SignalUtilitiesKit/TSGroupModel.h>
|
||||
#import <SessionMessagingKit/OWSBlockingManager.h>
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
#import <SessionMessagingKit/SSKEnvironment.h>
|
||||
#import <SessionMessagingKit/TSDatabaseView.h>
|
||||
#import <SessionMessagingKit/TSIncomingMessage.h>
|
||||
#import <SessionMessagingKit/TSOutgoingMessage.h>
|
||||
#import <SessionMessagingKit/TSThread.h>
|
||||
#import <SessionMessagingKit/TSGroupThread.h>
|
||||
#import <SessionMessagingKit/TSGroupModel.h>
|
||||
#import <YapDatabase/YapDatabase.h>
|
||||
#import <YapDatabase/YapDatabaseAutoView.h>
|
||||
#import <YapDatabase/YapDatabaseViewChange.h>
|
||||
|
@ -226,7 +223,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
|
||||
- (instancetype)initWithThread:(TSThread *)thread
|
||||
focusMessageIdOnOpen:(nullable NSString *)focusMessageIdOnOpen
|
||||
isRSSFeed:(BOOL)isRSSFeed
|
||||
delegate:(id<ConversationViewModelDelegate>)delegate
|
||||
{
|
||||
self = [super init];
|
||||
|
@ -242,7 +238,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
_persistedViewItems = @[];
|
||||
_unsavedOutgoingMessages = @[];
|
||||
self.focusMessageIdOnOpen = focusMessageIdOnOpen;
|
||||
_isRSSFeed = isRSSFeed;
|
||||
_viewState = [[ConversationViewState alloc] initWithViewItems:@[]];
|
||||
|
||||
[self configure];
|
||||
|
@ -269,11 +264,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
return self.primaryStorage.dbReadWriteConnection;
|
||||
}
|
||||
|
||||
- (OWSContactsManager *)contactsManager
|
||||
{
|
||||
return (OWSContactsManager *)SSKEnvironment.shared.contactsManager;
|
||||
}
|
||||
|
||||
- (OWSBlockingManager *)blockingManager
|
||||
{
|
||||
return OWSBlockingManager.sharedManager;
|
||||
|
@ -304,10 +294,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
selector:@selector(applicationDidEnterBackground:)
|
||||
name:OWSApplicationDidEnterBackgroundNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(signalAccountsDidChange:)
|
||||
name:OWSContactsManagerSignalAccountsDidChangeNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(typingIndicatorStateDidChange:)
|
||||
name:[OWSTypingIndicatorsImpl typingIndicatorStateDidChange]
|
||||
|
@ -530,7 +516,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
|
||||
ThreadDynamicInteractions *dynamicInteractions =
|
||||
[ThreadUtil ensureDynamicInteractionsForThread:self.thread
|
||||
contactsManager:self.contactsManager
|
||||
blockingManager:self.blockingManager
|
||||
dbConnection:self.editingDatabaseConnection
|
||||
hideUnreadMessagesIndicator:self.hasClearedUnreadMessagesIndicator
|
||||
|
@ -1023,24 +1008,10 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
return;
|
||||
}
|
||||
|
||||
// Many OWSProfileManager methods aren't safe to call from inside a database
|
||||
// transaction, so do this work now.
|
||||
//
|
||||
// TODO: It'd be nice if these methods took a transaction.
|
||||
BOOL hasLocalProfile = [self.profileManager hasLocalProfile];
|
||||
BOOL isThreadInProfileWhitelist = [self.profileManager isThreadInProfileWhitelist:self.thread];
|
||||
BOOL hasUnwhitelistedMember = NO;
|
||||
for (NSString *recipientId in self.thread.recipientIdentifiers) {
|
||||
if (![self.profileManager isUserInProfileWhitelist:recipientId]) {
|
||||
hasUnwhitelistedMember = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ConversationProfileState *conversationProfileState = [ConversationProfileState new];
|
||||
conversationProfileState.hasLocalProfile = hasLocalProfile;
|
||||
conversationProfileState.isThreadInProfileWhitelist = isThreadInProfileWhitelist;
|
||||
conversationProfileState.hasUnwhitelistedMember = hasUnwhitelistedMember;
|
||||
conversationProfileState.hasLocalProfile = YES;
|
||||
conversationProfileState.isThreadInProfileWhitelist = YES;
|
||||
conversationProfileState.hasUnwhitelistedMember = NO;
|
||||
self.conversationProfileState = conversationProfileState;
|
||||
}
|
||||
|
||||
|
@ -1067,142 +1038,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
return nil;
|
||||
}
|
||||
|
||||
- (nullable OWSContactOffersInteraction *)
|
||||
tryToBuildContactOffersInteractionWithTransaction:(YapDatabaseReadTransaction *)transaction
|
||||
loadedInteractions:(NSArray<TSInteraction *> *)loadedInteractions
|
||||
canLoadMoreItems:(BOOL)canLoadMoreItems
|
||||
{
|
||||
OWSAssertDebug(transaction);
|
||||
OWSAssertDebug(self.conversationProfileState);
|
||||
|
||||
if (canLoadMoreItems) {
|
||||
// Only show contact offers at the start of the conversation.
|
||||
return nil;
|
||||
}
|
||||
|
||||
BOOL hasLocalProfile = self.conversationProfileState.hasLocalProfile;
|
||||
BOOL isThreadInProfileWhitelist = self.conversationProfileState.isThreadInProfileWhitelist;
|
||||
BOOL hasUnwhitelistedMember = self.conversationProfileState.hasUnwhitelistedMember;
|
||||
|
||||
TSThread *thread = self.thread;
|
||||
BOOL isContactThread = [thread isKindOfClass:[TSContactThread class]];
|
||||
if (!isContactThread) {
|
||||
return nil;
|
||||
}
|
||||
TSContactThread *contactThread = (TSContactThread *)thread;
|
||||
if (contactThread.hasDismissedOffers) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *localNumber = [self.tsAccountManager localNumber];
|
||||
OWSAssertDebug(localNumber.length > 0);
|
||||
|
||||
TSInteraction *firstCallOrMessage = [self firstCallOrMessageForLoadedInteractions:loadedInteractions];
|
||||
if (!firstCallOrMessage) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
BOOL hasTooManyOutgoingMessagesToBlock;
|
||||
if (self.hasTooManyOutgoingMessagesToBlockCached) {
|
||||
hasTooManyOutgoingMessagesToBlock = YES;
|
||||
} else {
|
||||
NSUInteger outgoingMessageCount =
|
||||
[[TSDatabaseView threadOutgoingMessageDatabaseView:transaction] numberOfItemsInGroup:thread.uniqueId];
|
||||
|
||||
const int kMaxBlockOfferOutgoingMessageCount = 10;
|
||||
hasTooManyOutgoingMessagesToBlock = (outgoingMessageCount > kMaxBlockOfferOutgoingMessageCount);
|
||||
self.hasTooManyOutgoingMessagesToBlockCached = hasTooManyOutgoingMessagesToBlock;
|
||||
}
|
||||
|
||||
BOOL shouldHaveBlockOffer = YES;
|
||||
BOOL shouldHaveAddToContactsOffer = YES;
|
||||
BOOL shouldHaveAddToProfileWhitelistOffer = YES;
|
||||
|
||||
NSString *recipientId = ((TSContactThread *)thread).contactIdentifier;
|
||||
|
||||
if ([recipientId isEqualToString:localNumber]) {
|
||||
// Don't add self to contacts.
|
||||
shouldHaveAddToContactsOffer = NO;
|
||||
// Don't bother to block self.
|
||||
shouldHaveBlockOffer = NO;
|
||||
// Don't bother adding self to profile whitelist.
|
||||
shouldHaveAddToProfileWhitelistOffer = NO;
|
||||
} else {
|
||||
if ([[self.blockingManager blockedPhoneNumbers] containsObject:recipientId]) {
|
||||
// Only create "add to contacts" offers for users which are not already blocked.
|
||||
shouldHaveAddToContactsOffer = NO;
|
||||
// Only create block offers for users which are not already blocked.
|
||||
shouldHaveBlockOffer = NO;
|
||||
// Don't create profile whitelist offers for users which are not already blocked.
|
||||
shouldHaveAddToProfileWhitelistOffer = NO;
|
||||
}
|
||||
|
||||
if ([self.contactsManager hasSignalAccountForRecipientId:recipientId]) {
|
||||
// Only create "add to contacts" offers for non-contacts.
|
||||
shouldHaveAddToContactsOffer = NO;
|
||||
// Only create block offers for non-contacts.
|
||||
shouldHaveBlockOffer = NO;
|
||||
// Don't create profile whitelist offers for non-contacts.
|
||||
shouldHaveAddToProfileWhitelistOffer = NO;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasTooManyOutgoingMessagesToBlock) {
|
||||
// If the user has sent more than N messages, don't show a block offer.
|
||||
shouldHaveBlockOffer = NO;
|
||||
}
|
||||
|
||||
BOOL hasOutgoingBeforeIncomingInteraction = [firstCallOrMessage isKindOfClass:[TSOutgoingMessage class]];
|
||||
if ([firstCallOrMessage isKindOfClass:[TSCall class]]) {
|
||||
TSCall *call = (TSCall *)firstCallOrMessage;
|
||||
hasOutgoingBeforeIncomingInteraction
|
||||
= (call.callType == RPRecentCallTypeOutgoing || call.callType == RPRecentCallTypeOutgoingIncomplete);
|
||||
}
|
||||
if (hasOutgoingBeforeIncomingInteraction) {
|
||||
// If there is an outgoing message before an incoming message
|
||||
// the local user initiated this conversation, don't show a block offer.
|
||||
shouldHaveBlockOffer = NO;
|
||||
}
|
||||
|
||||
if (!hasLocalProfile || isThreadInProfileWhitelist) {
|
||||
// Don't show offer if thread is local user hasn't configured their profile.
|
||||
// Don't show offer if thread is already in profile whitelist.
|
||||
shouldHaveAddToProfileWhitelistOffer = NO;
|
||||
} else if (thread.isGroupThread && !hasUnwhitelistedMember) {
|
||||
// Don't show offer in group thread if all members are already individually
|
||||
// whitelisted.
|
||||
shouldHaveAddToProfileWhitelistOffer = NO;
|
||||
}
|
||||
|
||||
BOOL shouldHaveContactOffers
|
||||
= (shouldHaveBlockOffer || shouldHaveAddToContactsOffer || shouldHaveAddToProfileWhitelistOffer);
|
||||
if (!shouldHaveContactOffers) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// We want the offers to be the first interactions in their
|
||||
// conversation's timeline, so we back-date them to slightly before
|
||||
// the first message - or at an arbitrary old timestamp if the
|
||||
// conversation has no messages.
|
||||
uint64_t contactOffersTimestamp = firstCallOrMessage.timestamp - 1;
|
||||
// This view model uses the "unique id" to identify this interaction,
|
||||
// but the interaction is never saved in the database so the specific
|
||||
// value doesn't matter.
|
||||
NSString *uniqueId = @"contact-offers";
|
||||
OWSContactOffersInteraction *offersMessage =
|
||||
[[OWSContactOffersInteraction alloc] initInteractionWithUniqueId:uniqueId
|
||||
timestamp:contactOffersTimestamp
|
||||
thread:thread
|
||||
hasBlockOffer:shouldHaveBlockOffer
|
||||
hasAddToContactsOffer:shouldHaveAddToContactsOffer
|
||||
hasAddToProfileWhitelistOffer:shouldHaveAddToProfileWhitelistOffer
|
||||
recipientId:recipientId
|
||||
beforeInteractionId:firstCallOrMessage.uniqueId];
|
||||
|
||||
OWSLogInfo(@"Creating contact offers: %@ (%llu)", offersMessage.uniqueId, offersMessage.sortId);
|
||||
return offersMessage;
|
||||
}
|
||||
|
||||
// This is a key method. It builds or rebuilds the list of
|
||||
// cell view models.
|
||||
//
|
||||
|
@ -1214,7 +1049,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
|
||||
NSArray<NSString *> *loadedUniqueIds = [self.messageMapping loadedUniqueIds];
|
||||
BOOL isGroupThread = self.thread.isGroupThread;
|
||||
BOOL isRSSFeed = self.isRSSFeed;
|
||||
ConversationStyle *conversationStyle = self.delegate.conversationStyle;
|
||||
|
||||
[self ensureConversationProfileState];
|
||||
|
@ -1228,24 +1062,12 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
if (!viewItem) {
|
||||
viewItem = [[ConversationInteractionViewItem alloc] initWithInteraction:interaction
|
||||
isGroupThread:isGroupThread
|
||||
isRSSFeed:isRSSFeed
|
||||
transaction:transaction
|
||||
conversationStyle:conversationStyle];
|
||||
}
|
||||
OWSAssertDebug(!viewItemCache[interaction.uniqueId]);
|
||||
viewItemCache[interaction.uniqueId] = viewItem;
|
||||
[viewItems addObject:viewItem];
|
||||
TSMessage *message = (TSMessage *)viewItem.interaction;
|
||||
if (message.hasAttachmentsInNSE) {
|
||||
[SSKEnvironment.shared.attachmentDownloads downloadAttachmentsForMessage:message
|
||||
transaction:transaction
|
||||
success:^(NSArray<TSAttachmentStream *> *attachmentStreams) {
|
||||
OWSLogInfo(@"Successfully redownloaded attachment in thread: %@", message.thread);
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
OWSLogWarn(@"Failed to redownload message with error: %@", error);
|
||||
}];
|
||||
}
|
||||
|
||||
return viewItem;
|
||||
};
|
||||
|
@ -1280,22 +1102,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
[interactionIds addObject:interaction.uniqueId];
|
||||
}
|
||||
|
||||
OWSContactOffersInteraction *_Nullable offers = nil;
|
||||
if (offers && [interactionIds containsObject:offers.beforeInteractionId]) {
|
||||
id<ConversationViewItem> offersItem = tryToAddViewItem(offers, transaction);
|
||||
if ([offersItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]) {
|
||||
OWSContactOffersInteraction *oldOffers = (OWSContactOffersInteraction *)offersItem.interaction;
|
||||
BOOL didChange = (oldOffers.hasBlockOffer != offers.hasBlockOffer
|
||||
|| oldOffers.hasAddToContactsOffer != offers.hasAddToContactsOffer
|
||||
|| oldOffers.hasAddToProfileWhitelistOffer != offers.hasAddToProfileWhitelistOffer);
|
||||
if (didChange) {
|
||||
[offersItem clearCachedLayoutState];
|
||||
}
|
||||
} else {
|
||||
OWSFailDebug(@"Unexpected offers item: %@", offersItem.interaction.class);
|
||||
}
|
||||
}
|
||||
|
||||
for (TSInteraction *interaction in interactions) {
|
||||
tryToAddViewItem(interaction, transaction);
|
||||
}
|
||||
|
@ -1534,8 +1340,7 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
}
|
||||
|
||||
if (shouldShowSenderName) {
|
||||
senderName = [self.contactsManager attributedContactOrProfileNameForPhoneIdentifier:incomingSenderId primaryAttributes:[OWSMessageBubbleView senderNamePrimaryAttributes]
|
||||
secondaryAttributes:[OWSMessageBubbleView senderNameSecondaryAttributes]];
|
||||
senderName = [[NSAttributedString alloc] initWithString:[SSKEnvironment.shared.profileManager profileNameForRecipientWithID:incomingSenderId avoidingWriteTransaction:YES]];
|
||||
|
||||
if ([self.thread isKindOfClass:[TSGroupThread class]]) {
|
||||
TSGroupThread *groupThread = (TSGroupThread *)self.thread;
|
||||
|
@ -1558,9 +1363,7 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
// the next message has the same sender avatar and
|
||||
// no "date break" separates us.
|
||||
shouldShowSenderAvatar = YES;
|
||||
if (viewItem.isRSSFeed) {
|
||||
shouldShowSenderAvatar = NO;
|
||||
} else if (previousViewItem && previousViewItem.interaction.interactionType == interactionType) {
|
||||
if (previousViewItem && previousViewItem.interaction.interactionType == interactionType) {
|
||||
shouldShowSenderAvatar = (![NSObject isNullableObject:previousIncomingSenderId equalTo:incomingSenderId]);
|
||||
}
|
||||
}
|
||||
|
@ -1588,7 +1391,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
// Because the message isn't yet saved, we don't have sufficient information to build
|
||||
// in-memory placeholder for message types more complex than plain text.
|
||||
OWSAssertDebug(outgoingMessage.attachmentIds.count == 0);
|
||||
OWSAssertDebug(outgoingMessage.contactShare == nil);
|
||||
|
||||
NSMutableArray<TSOutgoingMessage *> *unsavedOutgoingMessages = [self.unsavedOutgoingMessages mutableCopy];
|
||||
[unsavedOutgoingMessages addObject:outgoingMessage];
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#import "DateUtil.h"
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSFormat.h>
|
||||
#import <SignalUtilitiesKit/NSString+SSK.h>
|
||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface DomainFrontingCountryViewController : OWSViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,98 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DomainFrontingCountryViewController.h"
|
||||
#import "OWSCountryMetadata.h"
|
||||
#import "OWSTableViewController.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Theme.h>
|
||||
#import <SignalUtilitiesKit/OWSSignalService.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface DomainFrontingCountryViewController ()
|
||||
|
||||
@property (nonatomic, readonly) OWSTableViewController *tableViewController;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation DomainFrontingCountryViewController
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
self.title = NSLocalizedString(
|
||||
@"CENSORSHIP_CIRCUMVENTION_COUNTRY_VIEW_TITLE", @"Title for the 'censorship circumvention country' view.");
|
||||
|
||||
self.view.backgroundColor = Theme.backgroundColor;
|
||||
|
||||
[self createViews];
|
||||
}
|
||||
|
||||
- (void)createViews
|
||||
{
|
||||
_tableViewController = [OWSTableViewController new];
|
||||
[self.view addSubview:self.tableViewController.view];
|
||||
[self.tableViewController.view autoPinEdgeToSuperviewSafeArea:ALEdgeLeading];
|
||||
[self.tableViewController.view autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing];
|
||||
[_tableViewController.view autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.view withOffset:0.0f];
|
||||
[_tableViewController.view autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.view withOffset:0.0f];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - Table Contents
|
||||
|
||||
- (void)updateTableContents
|
||||
{
|
||||
OWSTableContents *contents = [OWSTableContents new];
|
||||
|
||||
NSString *currentCountryCode = OWSSignalService.sharedInstance.manualCensorshipCircumventionCountryCode;
|
||||
|
||||
__weak DomainFrontingCountryViewController *weakSelf = self;
|
||||
|
||||
OWSTableSection *section = [OWSTableSection new];
|
||||
section.headerTitle = NSLocalizedString(
|
||||
@"DOMAIN_FRONTING_COUNTRY_VIEW_SECTION_HEADER", @"Section title for the 'domain fronting country' view.");
|
||||
for (OWSCountryMetadata *countryMetadata in [OWSCountryMetadata allCountryMetadatas]) {
|
||||
[section addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
UITableViewCell *cell = [OWSTableItem newCell];
|
||||
[OWSTableItem configureCell:cell];
|
||||
cell.textLabel.text = countryMetadata.localizedCountryName;
|
||||
|
||||
if ([countryMetadata.countryCode isEqualToString:currentCountryCode]) {
|
||||
cell.accessoryType = UITableViewCellAccessoryCheckmark;
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
actionBlock:^{
|
||||
[weakSelf selectCountry:countryMetadata];
|
||||
}]];
|
||||
}
|
||||
[contents addSection:section];
|
||||
|
||||
self.tableViewController.contents = contents;
|
||||
}
|
||||
|
||||
- (void)selectCountry:(OWSCountryMetadata *)countryMetadata
|
||||
{
|
||||
OWSAssertDebug(countryMetadata);
|
||||
|
||||
OWSSignalService.sharedInstance.manualCensorshipCircumventionCountryCode = countryMetadata.countryCode;
|
||||
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,15 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FingerprintViewController : OWSViewController
|
||||
|
||||
+ (void)presentFromViewController:(UIViewController *)viewController recipientId:(NSString *)recipientId;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,558 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FingerprintViewController.h"
|
||||
#import "FingerprintViewScanController.h"
|
||||
#import "OWSBezierPathView.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/OWSError.h>
|
||||
#import <SignalUtilitiesKit/OWSFingerprint.h>
|
||||
#import <SignalUtilitiesKit/OWSFingerprintBuilder.h>
|
||||
#import <SignalUtilitiesKit/OWSIdentityManager.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+SessionStore.h>
|
||||
#import <SignalUtilitiesKit/TSAccountManager.h>
|
||||
#import <SignalUtilitiesKit/TSInfoMessage.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef void (^CustomLayoutBlock)(void);
|
||||
|
||||
@interface CustomLayoutView : UIView
|
||||
|
||||
@property (nonatomic) CustomLayoutBlock layoutBlock;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation CustomLayoutView
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
if (self = [super initWithCoder:aDecoder]) {
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
self.layoutBlock();
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface FingerprintViewController () <OWSCompareSafetyNumbersActivityDelegate>
|
||||
|
||||
@property (nonatomic) NSString *recipientId;
|
||||
@property (nonatomic) NSData *identityKey;
|
||||
@property (nonatomic) TSAccountManager *accountManager;
|
||||
@property (nonatomic) OWSFingerprint *fingerprint;
|
||||
@property (nonatomic) NSString *contactName;
|
||||
|
||||
@property (nonatomic) UIBarButtonItem *shareButton;
|
||||
|
||||
@property (nonatomic) UILabel *verificationStateLabel;
|
||||
@property (nonatomic) UILabel *verifyUnverifyButtonLabel;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation FingerprintViewController
|
||||
|
||||
+ (void)presentFromViewController:(UIViewController *)viewController recipientId:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
OWSRecipientIdentity *_Nullable recipientIdentity =
|
||||
[[OWSIdentityManager sharedManager] recipientIdentityForRecipientId:recipientId];
|
||||
if (!recipientIdentity) {
|
||||
[OWSAlerts showAlertWithTitle:NSLocalizedString(@"CANT_VERIFY_IDENTITY_ALERT_TITLE",
|
||||
@"Title for alert explaining that a user cannot be verified.")
|
||||
message:NSLocalizedString(@"CANT_VERIFY_IDENTITY_ALERT_MESSAGE",
|
||||
@"Message for alert explaining that a user cannot be verified.")];
|
||||
return;
|
||||
}
|
||||
|
||||
FingerprintViewController *fingerprintViewController = [FingerprintViewController new];
|
||||
[fingerprintViewController configureWithRecipientId:recipientId];
|
||||
OWSNavigationController *navigationController =
|
||||
[[OWSNavigationController alloc] initWithRootViewController:fingerprintViewController];
|
||||
[viewController presentViewController:navigationController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_accountManager = [TSAccountManager sharedInstance];
|
||||
|
||||
[self observeNotifications];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)observeNotifications
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(identityStateDidChange:)
|
||||
name:kNSNotificationName_IdentityStateDidChange
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)configureWithRecipientId:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
self.recipientId = recipientId;
|
||||
|
||||
OWSContactsManager *contactsManager = Environment.shared.contactsManager;
|
||||
self.contactName = [contactsManager displayNameForPhoneIdentifier:recipientId];
|
||||
|
||||
OWSRecipientIdentity *_Nullable recipientIdentity =
|
||||
[[OWSIdentityManager sharedManager] recipientIdentityForRecipientId:recipientId];
|
||||
OWSAssertDebug(recipientIdentity);
|
||||
// By capturing the identity key when we enter these views, we prevent the edge case
|
||||
// where the user verifies a key that we learned about while this view was open.
|
||||
self.identityKey = recipientIdentity.identityKey;
|
||||
|
||||
OWSFingerprintBuilder *builder =
|
||||
[[OWSFingerprintBuilder alloc] initWithAccountManager:self.accountManager contactsManager:contactsManager];
|
||||
self.fingerprint =
|
||||
[builder fingerprintWithTheirSignalId:recipientId theirIdentityKey:recipientIdentity.identityKey];
|
||||
}
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
self.title = NSLocalizedString(@"PRIVACY_VERIFICATION_TITLE", @"Navbar title");
|
||||
|
||||
self.navigationItem.leftBarButtonItem =
|
||||
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop
|
||||
target:self
|
||||
action:@selector(closeButton)
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"stop")];
|
||||
self.shareButton =
|
||||
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction
|
||||
target:self
|
||||
action:@selector(didTapShareButton)
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"share")];
|
||||
self.navigationItem.rightBarButtonItem = self.shareButton;
|
||||
|
||||
[self createViews];
|
||||
}
|
||||
|
||||
- (void)createViews
|
||||
{
|
||||
self.view.backgroundColor = Theme.backgroundColor;
|
||||
|
||||
// Verify/Unverify Button
|
||||
UIView *verifyUnverifyButton = [UIView new];
|
||||
[verifyUnverifyButton
|
||||
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(verifyUnverifyButtonTapped:)]];
|
||||
[self.view addSubview:verifyUnverifyButton];
|
||||
[verifyUnverifyButton autoPinWidthToSuperview];
|
||||
[verifyUnverifyButton autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.view withOffset:0.0f];
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, verifyUnverifyButton);
|
||||
|
||||
UIView *verifyUnverifyPillbox = [UIView new];
|
||||
verifyUnverifyPillbox.backgroundColor = [UIColor ows_materialBlueColor];
|
||||
verifyUnverifyPillbox.layer.cornerRadius = 3.f;
|
||||
verifyUnverifyPillbox.clipsToBounds = YES;
|
||||
[verifyUnverifyButton addSubview:verifyUnverifyPillbox];
|
||||
[verifyUnverifyPillbox autoHCenterInSuperview];
|
||||
[verifyUnverifyPillbox autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:ScaleFromIPhone5To7Plus(10.f, 15.f)];
|
||||
[verifyUnverifyPillbox autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:ScaleFromIPhone5To7Plus(10.f, 20.f)];
|
||||
|
||||
UILabel *verifyUnverifyButtonLabel = [UILabel new];
|
||||
self.verifyUnverifyButtonLabel = verifyUnverifyButtonLabel;
|
||||
verifyUnverifyButtonLabel.font = [UIFont ows_mediumFontWithSize:ScaleFromIPhone5To7Plus(14.f, 20.f)];
|
||||
verifyUnverifyButtonLabel.textColor = [UIColor whiteColor];
|
||||
verifyUnverifyButtonLabel.textAlignment = NSTextAlignmentCenter;
|
||||
[verifyUnverifyPillbox addSubview:verifyUnverifyButtonLabel];
|
||||
[verifyUnverifyButtonLabel autoPinWidthToSuperviewWithMargin:ScaleFromIPhone5To7Plus(50.f, 50.f)];
|
||||
[verifyUnverifyButtonLabel autoPinHeightToSuperviewWithMargin:ScaleFromIPhone5To7Plus(8.f, 8.f)];
|
||||
|
||||
// Learn More
|
||||
UIView *learnMoreButton = [UIView new];
|
||||
[learnMoreButton
|
||||
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(learnMoreButtonTapped:)]];
|
||||
[self.view addSubview:learnMoreButton];
|
||||
[learnMoreButton autoPinWidthToSuperview];
|
||||
[learnMoreButton autoPinEdge:ALEdgeBottom toEdge:ALEdgeTop ofView:verifyUnverifyButton withOffset:0];
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, learnMoreButton);
|
||||
|
||||
UILabel *learnMoreLabel = [UILabel new];
|
||||
learnMoreLabel.attributedText = [[NSAttributedString alloc]
|
||||
initWithString:NSLocalizedString(@"PRIVACY_SAFETY_NUMBERS_LEARN_MORE",
|
||||
@"Label for a link to more information about safety numbers and verification.")
|
||||
attributes:@{
|
||||
NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid),
|
||||
}];
|
||||
learnMoreLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(13.f, 16.f)];
|
||||
learnMoreLabel.textColor = [UIColor ows_materialBlueColor];
|
||||
learnMoreLabel.textAlignment = NSTextAlignmentCenter;
|
||||
[learnMoreButton addSubview:learnMoreLabel];
|
||||
[learnMoreLabel autoPinWidthToSuperviewWithMargin:16.f];
|
||||
[learnMoreLabel autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:ScaleFromIPhone5To7Plus(5.f, 10.f)];
|
||||
[learnMoreLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:ScaleFromIPhone5To7Plus(5.f, 10.f)];
|
||||
|
||||
// Instructions
|
||||
NSString *instructionsFormat = NSLocalizedString(@"PRIVACY_VERIFICATION_INSTRUCTIONS",
|
||||
@"Paragraph(s) shown alongside the safety number when verifying privacy with {{contact name}}");
|
||||
UILabel *instructionsLabel = [UILabel new];
|
||||
instructionsLabel.text = [NSString stringWithFormat:instructionsFormat, self.contactName];
|
||||
instructionsLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 14.f)];
|
||||
instructionsLabel.textColor = Theme.secondaryColor;
|
||||
instructionsLabel.textAlignment = NSTextAlignmentCenter;
|
||||
instructionsLabel.numberOfLines = 0;
|
||||
instructionsLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
[self.view addSubview:instructionsLabel];
|
||||
[instructionsLabel autoPinWidthToSuperviewWithMargin:16.f];
|
||||
[instructionsLabel autoPinEdge:ALEdgeBottom toEdge:ALEdgeTop ofView:learnMoreButton withOffset:0];
|
||||
|
||||
// Fingerprint Label
|
||||
UILabel *fingerprintLabel = [UILabel new];
|
||||
fingerprintLabel.text = self.fingerprint.displayableText;
|
||||
fingerprintLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:ScaleFromIPhone5To7Plus(20.f, 23.f)];
|
||||
fingerprintLabel.textColor = Theme.secondaryColor;
|
||||
fingerprintLabel.numberOfLines = 3;
|
||||
fingerprintLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
fingerprintLabel.adjustsFontSizeToFitWidth = YES;
|
||||
[fingerprintLabel
|
||||
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(fingerprintLabelTapped:)]];
|
||||
fingerprintLabel.userInteractionEnabled = YES;
|
||||
[self.view addSubview:fingerprintLabel];
|
||||
[fingerprintLabel autoPinWidthToSuperviewWithMargin:ScaleFromIPhone5To7Plus(50.f, 60.f)];
|
||||
[fingerprintLabel autoPinEdge:ALEdgeBottom
|
||||
toEdge:ALEdgeTop
|
||||
ofView:instructionsLabel
|
||||
withOffset:-ScaleFromIPhone5To7Plus(8.f, 15.f)];
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, fingerprintLabel);
|
||||
|
||||
// Fingerprint Image
|
||||
CustomLayoutView *fingerprintView = [CustomLayoutView new];
|
||||
[self.view addSubview:fingerprintView];
|
||||
[fingerprintView autoPinWidthToSuperview];
|
||||
[fingerprintView autoPinEdge:ALEdgeBottom
|
||||
toEdge:ALEdgeTop
|
||||
ofView:fingerprintLabel
|
||||
withOffset:-ScaleFromIPhone5To7Plus(10.f, 15.f)];
|
||||
[fingerprintView
|
||||
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(fingerprintViewTapped:)]];
|
||||
fingerprintView.userInteractionEnabled = YES;
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, fingerprintView);
|
||||
|
||||
OWSBezierPathView *fingerprintCircle = [OWSBezierPathView new];
|
||||
[fingerprintCircle setConfigureShapeLayerBlock:^(CAShapeLayer *layer, CGRect bounds) {
|
||||
layer.fillColor = Theme.offBackgroundColor.CGColor;
|
||||
CGFloat size = MIN(bounds.size.width, bounds.size.height);
|
||||
CGRect circle = CGRectMake((bounds.size.width - size) * 0.5f, (bounds.size.height - size) * 0.5f, size, size);
|
||||
layer.path = [UIBezierPath bezierPathWithOvalInRect:circle].CGPath;
|
||||
}];
|
||||
[fingerprintView addSubview:fingerprintCircle];
|
||||
[fingerprintCircle ows_autoPinToSuperviewEdges];
|
||||
|
||||
UIImageView *fingerprintImageView = [UIImageView new];
|
||||
fingerprintImageView.image = self.fingerprint.image;
|
||||
// Don't antialias QR Codes.
|
||||
fingerprintImageView.layer.magnificationFilter = kCAFilterNearest;
|
||||
fingerprintImageView.layer.minificationFilter = kCAFilterNearest;
|
||||
[fingerprintView addSubview:fingerprintImageView];
|
||||
|
||||
UILabel *scanLabel = [UILabel new];
|
||||
scanLabel.text = NSLocalizedString(@"PRIVACY_TAP_TO_SCAN", @"Button that shows the 'scan with camera' view.");
|
||||
scanLabel.font = [UIFont ows_mediumFontWithSize:ScaleFromIPhone5To7Plus(14.f, 16.f)];
|
||||
scanLabel.textColor = Theme.secondaryColor;
|
||||
[scanLabel sizeToFit];
|
||||
[fingerprintView addSubview:scanLabel];
|
||||
|
||||
fingerprintView.layoutBlock = ^{
|
||||
CGFloat size = round(MIN(fingerprintView.width, fingerprintView.height) * 0.675f);
|
||||
fingerprintImageView.frame = CGRectMake(
|
||||
round((fingerprintView.width - size) * 0.5f), round((fingerprintView.height - size) * 0.5f), size, size);
|
||||
CGFloat scanY = round(fingerprintImageView.bottom
|
||||
+ ((fingerprintView.height - fingerprintImageView.bottom) - scanLabel.height) * 0.33f);
|
||||
scanLabel.frame = CGRectMake(
|
||||
round((fingerprintView.width - scanLabel.width) * 0.5f), scanY, scanLabel.width, scanLabel.height);
|
||||
};
|
||||
|
||||
// Verification State
|
||||
UILabel *verificationStateLabel = [UILabel new];
|
||||
self.verificationStateLabel = verificationStateLabel;
|
||||
verificationStateLabel.font = [UIFont ows_mediumFontWithSize:ScaleFromIPhone5To7Plus(16.f, 20.f)];
|
||||
verificationStateLabel.textColor = Theme.secondaryColor;
|
||||
verificationStateLabel.textAlignment = NSTextAlignmentCenter;
|
||||
verificationStateLabel.numberOfLines = 0;
|
||||
verificationStateLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
[self.view addSubview:verificationStateLabel];
|
||||
[verificationStateLabel autoPinWidthToSuperviewWithMargin:16.f];
|
||||
// Bind height of label to height of two lines of text.
|
||||
// This should always be sufficient, and will prevent the view's
|
||||
// layout from changing if the user is marked as verified or not
|
||||
// verified.
|
||||
[verificationStateLabel autoSetDimension:ALDimensionHeight
|
||||
toSize:round(verificationStateLabel.font.lineHeight * 2.25f)];
|
||||
[verificationStateLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.view withOffset:ScaleFromIPhone5To7Plus(15.f, 20.f)];
|
||||
[verificationStateLabel autoPinEdge:ALEdgeBottom
|
||||
toEdge:ALEdgeTop
|
||||
ofView:fingerprintView
|
||||
withOffset:-ScaleFromIPhone5To7Plus(10.f, 15.f)];
|
||||
|
||||
[self updateVerificationStateLabel];
|
||||
}
|
||||
|
||||
- (void)updateVerificationStateLabel
|
||||
{
|
||||
OWSAssertDebug(self.recipientId.length > 0);
|
||||
|
||||
BOOL isVerified = [[OWSIdentityManager sharedManager] verificationStateForRecipientId:self.recipientId]
|
||||
== OWSVerificationStateVerified;
|
||||
|
||||
if (isVerified) {
|
||||
NSMutableAttributedString *labelText = [NSMutableAttributedString new];
|
||||
|
||||
if (isVerified) {
|
||||
// Show a "checkmark" if this user is verified.
|
||||
[labelText
|
||||
appendAttributedString:[[NSAttributedString alloc]
|
||||
initWithString:LocalizationNotNeeded(@"\uf00c ")
|
||||
attributes:@{
|
||||
NSFontAttributeName : [UIFont
|
||||
ows_fontAwesomeFont:self.verificationStateLabel.font.pointSize],
|
||||
}]];
|
||||
}
|
||||
|
||||
[labelText
|
||||
appendAttributedString:
|
||||
[[NSAttributedString alloc]
|
||||
initWithString:[NSString stringWithFormat:NSLocalizedString(@"PRIVACY_IDENTITY_IS_VERIFIED_FORMAT",
|
||||
@"Label indicating that the user is verified. Embeds "
|
||||
@"{{the user's name or phone number}}."),
|
||||
self.contactName]]];
|
||||
self.verificationStateLabel.attributedText = labelText;
|
||||
|
||||
self.verifyUnverifyButtonLabel.text = NSLocalizedString(
|
||||
@"PRIVACY_UNVERIFY_BUTTON", @"Button that lets user mark another user's identity as unverified.");
|
||||
} else {
|
||||
self.verificationStateLabel.text = [NSString
|
||||
stringWithFormat:NSLocalizedString(@"PRIVACY_IDENTITY_IS_NOT_VERIFIED_FORMAT",
|
||||
@"Label indicating that the user is not verified. Embeds {{the user's name or phone "
|
||||
@"number}}."),
|
||||
self.contactName];
|
||||
|
||||
NSMutableAttributedString *buttonText = [NSMutableAttributedString new];
|
||||
// Show a "checkmark" if this user is not verified.
|
||||
[buttonText
|
||||
appendAttributedString:[[NSAttributedString alloc]
|
||||
initWithString:LocalizationNotNeeded(@"\uf00c ")
|
||||
attributes:@{
|
||||
NSFontAttributeName : [UIFont
|
||||
ows_fontAwesomeFont:self.verifyUnverifyButtonLabel.font.pointSize],
|
||||
}]];
|
||||
[buttonText appendAttributedString:
|
||||
[[NSAttributedString alloc]
|
||||
initWithString:NSLocalizedString(@"PRIVACY_VERIFY_BUTTON",
|
||||
@"Button that lets user mark another user's identity as verified.")]];
|
||||
self.verifyUnverifyButtonLabel.attributedText = buttonText;
|
||||
}
|
||||
|
||||
[self.view setNeedsLayout];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)showSharingActivityWithCompletion:(nullable void (^)(void))completionHandler
|
||||
{
|
||||
OWSLogDebug(@"Sharing safety numbers");
|
||||
|
||||
OWSCompareSafetyNumbersActivity *compareActivity = [[OWSCompareSafetyNumbersActivity alloc] initWithDelegate:self];
|
||||
|
||||
NSString *shareFormat = NSLocalizedString(
|
||||
@"SAFETY_NUMBER_SHARE_FORMAT", @"Snippet to share {{safety number}} with a friend. sent e.g. via SMS");
|
||||
NSString *shareString = [NSString stringWithFormat:shareFormat, self.fingerprint.displayableText];
|
||||
|
||||
UIActivityViewController *activityController =
|
||||
[[UIActivityViewController alloc] initWithActivityItems:@[ shareString ]
|
||||
applicationActivities:@[ compareActivity ]];
|
||||
|
||||
activityController.completionWithItemsHandler = ^void(UIActivityType __nullable activityType,
|
||||
BOOL completed,
|
||||
NSArray *__nullable returnedItems,
|
||||
NSError *__nullable activityError) {
|
||||
if (completionHandler) {
|
||||
completionHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// This value was extracted by inspecting `activityType` in the activityController.completionHandler
|
||||
NSString *const iCloudActivityType = @"com.apple.CloudDocsUI.AddToiCloudDrive";
|
||||
activityController.excludedActivityTypes = @[
|
||||
UIActivityTypePostToFacebook,
|
||||
UIActivityTypePostToWeibo,
|
||||
UIActivityTypeAirDrop,
|
||||
UIActivityTypePostToTwitter,
|
||||
iCloudActivityType // This isn't being excluded. RADAR https://openradar.appspot.com/27493621
|
||||
];
|
||||
|
||||
[self presentViewController:activityController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - OWSCompareSafetyNumbersActivityDelegate
|
||||
|
||||
- (void)compareSafetyNumbersActivitySucceededWithActivity:(OWSCompareSafetyNumbersActivity *)activity
|
||||
{
|
||||
[self showVerificationSucceeded];
|
||||
}
|
||||
|
||||
- (void)compareSafetyNumbersActivity:(OWSCompareSafetyNumbersActivity *)activity failedWithError:(NSError *)error
|
||||
{
|
||||
[self showVerificationFailedWithError:error];
|
||||
}
|
||||
|
||||
- (void)showVerificationSucceeded
|
||||
{
|
||||
[FingerprintViewScanController showVerificationSucceeded:self
|
||||
identityKey:self.identityKey
|
||||
recipientId:self.recipientId
|
||||
contactName:self.contactName
|
||||
tag:self.logTag];
|
||||
}
|
||||
|
||||
- (void)showVerificationFailedWithError:(NSError *)error
|
||||
{
|
||||
|
||||
[FingerprintViewScanController showVerificationFailedWithError:error
|
||||
viewController:self
|
||||
retryBlock:nil
|
||||
cancelBlock:^{
|
||||
// Do nothing.
|
||||
}
|
||||
tag:self.logTag];
|
||||
}
|
||||
|
||||
#pragma mark - Action
|
||||
|
||||
- (void)closeButton
|
||||
{
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)didTapShareButton
|
||||
{
|
||||
[self showSharingActivityWithCompletion:nil];
|
||||
}
|
||||
|
||||
- (void)showScanner
|
||||
{
|
||||
FingerprintViewScanController *scanView = [FingerprintViewScanController new];
|
||||
[scanView configureWithRecipientId:self.recipientId];
|
||||
[self.navigationController pushViewController:scanView animated:YES];
|
||||
}
|
||||
|
||||
- (void)learnMoreButtonTapped:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateRecognized) {
|
||||
NSString *learnMoreURL = @"https://support.signal.org/hc/en-us/articles/"
|
||||
@"213134107";
|
||||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:learnMoreURL]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)fingerprintLabelTapped:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateRecognized) {
|
||||
[self showSharingActivityWithCompletion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)fingerprintViewTapped:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateRecognized) {
|
||||
[self showScanner];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)verifyUnverifyButtonTapped:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateRecognized) {
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
BOOL isVerified = [[OWSIdentityManager sharedManager] verificationStateForRecipientId:self.recipientId
|
||||
transaction:transaction]
|
||||
== OWSVerificationStateVerified;
|
||||
|
||||
OWSVerificationState newVerificationState
|
||||
= (isVerified ? OWSVerificationStateDefault : OWSVerificationStateVerified);
|
||||
[[OWSIdentityManager sharedManager] setVerificationState:newVerificationState
|
||||
identityKey:self.identityKey
|
||||
recipientId:self.recipientId
|
||||
isUserInitiatedChange:YES
|
||||
transaction:transaction];
|
||||
}];
|
||||
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)identityStateDidChange:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self updateVerificationStateLabel];
|
||||
}
|
||||
|
||||
#pragma mark - Orientation
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
|
||||
{
|
||||
return UIInterfaceOrientationMaskPortrait;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FingerprintViewScanController : OWSViewController
|
||||
|
||||
- (void)configureWithRecipientId:(NSString *)recipientId NS_SWIFT_NAME(configure(recipientId:));
|
||||
|
||||
+ (void)showVerificationSucceeded:(UIViewController *)viewController
|
||||
identityKey:(NSData *)identityKey
|
||||
recipientId:(NSString *)recipientId
|
||||
contactName:(NSString *)contactName
|
||||
tag:(NSString *)tag;
|
||||
|
||||
+ (void)showVerificationFailedWithError:(NSError *)error
|
||||
viewController:(UIViewController *)viewController
|
||||
retryBlock:(void (^_Nullable)(void))retryBlock
|
||||
cancelBlock:(void (^_Nonnull)(void))cancelBlock
|
||||
tag:(NSString *)tag;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,258 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FingerprintViewScanController.h"
|
||||
#import "OWSQRCodeScanningViewController.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import "UIViewController+Permissions.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/OWSError.h>
|
||||
#import <SignalUtilitiesKit/OWSFingerprint.h>
|
||||
#import <SignalUtilitiesKit/OWSFingerprintBuilder.h>
|
||||
#import <SignalUtilitiesKit/OWSIdentityManager.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FingerprintViewScanController () <OWSQRScannerDelegate>
|
||||
|
||||
@property (nonatomic) TSAccountManager *accountManager;
|
||||
@property (nonatomic) NSString *recipientId;
|
||||
@property (nonatomic) NSData *identityKey;
|
||||
@property (nonatomic) OWSFingerprint *fingerprint;
|
||||
@property (nonatomic) NSString *contactName;
|
||||
@property (nonatomic) OWSQRCodeScanningViewController *qrScanningController;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation FingerprintViewScanController
|
||||
|
||||
- (void)configureWithRecipientId:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
self.recipientId = recipientId;
|
||||
self.accountManager = [TSAccountManager sharedInstance];
|
||||
|
||||
OWSContactsManager *contactsManager = Environment.shared.contactsManager;
|
||||
self.contactName = [contactsManager displayNameForPhoneIdentifier:recipientId];
|
||||
|
||||
OWSRecipientIdentity *_Nullable recipientIdentity =
|
||||
[[OWSIdentityManager sharedManager] recipientIdentityForRecipientId:recipientId];
|
||||
OWSAssertDebug(recipientIdentity);
|
||||
// By capturing the identity key when we enter these views, we prevent the edge case
|
||||
// where the user verifies a key that we learned about while this view was open.
|
||||
self.identityKey = recipientIdentity.identityKey;
|
||||
|
||||
OWSFingerprintBuilder *builder =
|
||||
[[OWSFingerprintBuilder alloc] initWithAccountManager:self.accountManager contactsManager:contactsManager];
|
||||
self.fingerprint =
|
||||
[builder fingerprintWithTheirSignalId:recipientId theirIdentityKey:recipientIdentity.identityKey];
|
||||
}
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
self.title = NSLocalizedString(@"SCAN_QR_CODE_VIEW_TITLE", @"Title for the 'scan QR code' view.");
|
||||
|
||||
[self createViews];
|
||||
}
|
||||
|
||||
- (void)createViews
|
||||
{
|
||||
self.view.backgroundColor = UIColor.blackColor;
|
||||
|
||||
self.qrScanningController = [OWSQRCodeScanningViewController new];
|
||||
self.qrScanningController.scanDelegate = self;
|
||||
[self.view addSubview:self.qrScanningController.view];
|
||||
[self.qrScanningController.view autoPinWidthToSuperview];
|
||||
[self.qrScanningController.view autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.view withOffset:0.0f];
|
||||
|
||||
UIView *footer = [UIView new];
|
||||
footer.backgroundColor = [UIColor colorWithWhite:0.25f alpha:1.f];
|
||||
[self.view addSubview:footer];
|
||||
[footer autoPinWidthToSuperview];
|
||||
[footer autoPinEdgeToSuperviewEdge:ALEdgeBottom];
|
||||
[footer autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.qrScanningController.view];
|
||||
|
||||
UILabel *cameraInstructionLabel = [UILabel new];
|
||||
cameraInstructionLabel.text
|
||||
= NSLocalizedString(@"SCAN_CODE_INSTRUCTIONS", @"label presented once scanning (camera) view is visible.");
|
||||
cameraInstructionLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(14.f, 18.f)];
|
||||
cameraInstructionLabel.textColor = [UIColor whiteColor];
|
||||
cameraInstructionLabel.textAlignment = NSTextAlignmentCenter;
|
||||
cameraInstructionLabel.numberOfLines = 0;
|
||||
cameraInstructionLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
[footer addSubview:cameraInstructionLabel];
|
||||
[cameraInstructionLabel autoPinWidthToSuperviewWithMargin:ScaleFromIPhone5To7Plus(16.f, 30.f)];
|
||||
CGFloat instructionsVMargin = ScaleFromIPhone5To7Plus(10.f, 20.f);
|
||||
[cameraInstructionLabel autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.view withOffset:instructionsVMargin];
|
||||
[cameraInstructionLabel autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:instructionsVMargin];
|
||||
}
|
||||
|
||||
#pragma mark - Action
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
[self ows_askForCameraPermissions:^(BOOL granted) {
|
||||
if (granted) {
|
||||
// Camera stops capturing when "sharing" while in capture mode.
|
||||
// Also, it's less obvious whats being "shared" at this point,
|
||||
// so just disable sharing when in capture mode.
|
||||
|
||||
OWSLogInfo(@"Showing Scanner");
|
||||
|
||||
[self.qrScanningController startCapture];
|
||||
} else {
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - OWSQRScannerDelegate
|
||||
|
||||
- (void)controller:(OWSQRCodeScanningViewController *)controller didDetectQRCodeWithData:(NSData *)data
|
||||
{
|
||||
[self verifyCombinedFingerprintData:data];
|
||||
}
|
||||
|
||||
- (void)verifyCombinedFingerprintData:(NSData *)combinedFingerprintData
|
||||
{
|
||||
NSError *error;
|
||||
if ([self.fingerprint matchesLogicalFingerprintsData:combinedFingerprintData error:&error]) {
|
||||
[self showVerificationSucceeded];
|
||||
} else {
|
||||
[self showVerificationFailedWithError:error];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showVerificationSucceeded
|
||||
{
|
||||
[self.class showVerificationSucceeded:self
|
||||
identityKey:self.identityKey
|
||||
recipientId:self.recipientId
|
||||
contactName:self.contactName
|
||||
tag:self.logTag];
|
||||
}
|
||||
|
||||
- (void)showVerificationFailedWithError:(NSError *)error
|
||||
{
|
||||
|
||||
[self.class showVerificationFailedWithError:error
|
||||
viewController:self
|
||||
retryBlock:^{
|
||||
[self.qrScanningController startCapture];
|
||||
}
|
||||
cancelBlock:^{
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
tag:self.logTag];
|
||||
}
|
||||
|
||||
+ (void)showVerificationSucceeded:(UIViewController *)viewController
|
||||
identityKey:(NSData *)identityKey
|
||||
recipientId:(NSString *)recipientId
|
||||
contactName:(NSString *)contactName
|
||||
tag:(NSString *)tag
|
||||
{
|
||||
OWSAssertDebug(viewController);
|
||||
OWSAssertDebug(identityKey.length > 0);
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
OWSAssertDebug(contactName.length > 0);
|
||||
OWSAssertDebug(tag.length > 0);
|
||||
|
||||
OWSLogInfo(@"%@ Successfully verified safety numbers.", tag);
|
||||
|
||||
NSString *successTitle = NSLocalizedString(@"SUCCESSFUL_VERIFICATION_TITLE", nil);
|
||||
NSString *descriptionFormat = NSLocalizedString(
|
||||
@"SUCCESSFUL_VERIFICATION_DESCRIPTION", @"Alert body after verifying privacy with {{other user's name}}");
|
||||
NSString *successDescription = [NSString stringWithFormat:descriptionFormat, contactName];
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:successTitle
|
||||
message:successDescription
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert
|
||||
addAction:[UIAlertAction
|
||||
actionWithTitle:NSLocalizedString(@"FINGERPRINT_SCAN_VERIFY_BUTTON",
|
||||
@"Button that marks user as verified after a successful fingerprint scan.")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[OWSIdentityManager.sharedManager setVerificationState:OWSVerificationStateVerified
|
||||
identityKey:identityKey
|
||||
recipientId:recipientId
|
||||
isUserInitiatedChange:YES];
|
||||
[viewController dismissViewControllerAnimated:true completion:nil];
|
||||
}]];
|
||||
UIAlertAction *dismissAction =
|
||||
[UIAlertAction actionWithTitle:CommonStrings.dismissButton
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[viewController dismissViewControllerAnimated:true completion:nil];
|
||||
}];
|
||||
[alert addAction:dismissAction];
|
||||
|
||||
[viewController presentAlert:alert];
|
||||
}
|
||||
|
||||
+ (void)showVerificationFailedWithError:(NSError *)error
|
||||
viewController:(UIViewController *)viewController
|
||||
retryBlock:(void (^_Nullable)(void))retryBlock
|
||||
cancelBlock:(void (^_Nonnull)(void))cancelBlock
|
||||
tag:(NSString *)tag
|
||||
{
|
||||
OWSAssertDebug(viewController);
|
||||
OWSAssertDebug(cancelBlock);
|
||||
OWSAssertDebug(tag.length > 0);
|
||||
|
||||
OWSLogInfo(@"%@ Failed to verify safety numbers.", tag);
|
||||
|
||||
NSString *_Nullable failureTitle;
|
||||
if (error.code != OWSErrorCodeUserError) {
|
||||
failureTitle = NSLocalizedString(@"FAILED_VERIFICATION_TITLE", @"alert title");
|
||||
} // else no title. We don't want to show a big scary "VERIFICATION FAILED" when it's just user error.
|
||||
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:failureTitle
|
||||
message:error.localizedDescription
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
if (retryBlock) {
|
||||
[alert addAction:[UIAlertAction actionWithTitle:[CommonStrings retryButton]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
retryBlock();
|
||||
}]];
|
||||
}
|
||||
|
||||
[alert addAction:[OWSAlerts cancelAction]];
|
||||
|
||||
[viewController presentAlert:alert];
|
||||
|
||||
OWSLogWarn(@"%@ Identity verification failed with error: %@", tag, error);
|
||||
}
|
||||
|
||||
- (void)dismissViewControllerAnimated:(BOOL)animated completion:(nullable void (^)(void))completion
|
||||
{
|
||||
self.qrScanningController.view.hidden = YES;
|
||||
|
||||
[super dismissViewControllerAnimated:animated completion:completion];
|
||||
}
|
||||
|
||||
#pragma mark - Orientation
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
|
||||
{
|
||||
return UIInterfaceOrientationMaskPortrait;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -35,7 +35,6 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
|
|||
public weak var delegate: GifPickerViewControllerDelegate?
|
||||
|
||||
let thread: TSThread
|
||||
let messageSender: MessageSender
|
||||
|
||||
let searchBar: SearchBar
|
||||
let layout: GifPickerLayout
|
||||
|
@ -60,9 +59,8 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
|
|||
}
|
||||
|
||||
@objc
|
||||
required init(thread: TSThread, messageSender: MessageSender) {
|
||||
required init(thread: TSThread) {
|
||||
self.thread = thread
|
||||
self.messageSender = messageSender
|
||||
|
||||
self.searchBar = SearchBar()
|
||||
self.layout = GifPickerLayout()
|
||||
|
@ -390,7 +388,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
|
|||
owsFailDebug("couldn't load asset.")
|
||||
return
|
||||
}
|
||||
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: rendition.utiType, imageQuality: .original)
|
||||
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: rendition.utiType, imageQuality: .medium)
|
||||
|
||||
strongSelf.dismiss(animated: true) {
|
||||
// Delegate presents view controllers, so it's important that *this* controller be dismissed before that occurs.
|
||||
|
|
|
@ -7,15 +7,9 @@ import SignalUtilitiesKit
|
|||
|
||||
@objc class GroupTableViewCell: UITableViewCell {
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private let avatarView = AvatarImageView()
|
||||
// private let avatarView = AvatarImageView()
|
||||
private let nameLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
|
||||
|
@ -30,14 +24,14 @@ import SignalUtilitiesKit
|
|||
|
||||
// Layout
|
||||
|
||||
avatarView.autoSetDimension(.width, toSize: CGFloat(kStandardAvatarSize))
|
||||
avatarView.autoPinToSquareAspectRatio()
|
||||
// avatarView.autoSetDimension(.width, toSize: CGFloat(kStandardAvatarSize))
|
||||
// avatarView.autoPinToSquareAspectRatio()
|
||||
|
||||
let textRows = UIStackView(arrangedSubviews: [nameLabel, subtitleLabel])
|
||||
textRows.axis = .vertical
|
||||
textRows.alignment = .leading
|
||||
|
||||
let columns = UIStackView(arrangedSubviews: [avatarView, textRows])
|
||||
let columns = UIStackView(arrangedSubviews: [ textRows ])
|
||||
columns.axis = .horizontal
|
||||
columns.alignment = .center
|
||||
columns.spacing = kContactCellAvatarTextMargin
|
||||
|
@ -62,11 +56,11 @@ import SignalUtilitiesKit
|
|||
|
||||
let groupMemberIds: [String] = thread.groupModel.groupMemberIds
|
||||
let groupMemberNames = groupMemberIds.map { (recipientId: String) in
|
||||
contactsManager.displayName(forPhoneIdentifier: recipientId)
|
||||
SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: recipientId, avoidingWriteTransaction: true)!
|
||||
}.joined(separator: ", ")
|
||||
self.subtitleLabel.text = groupMemberNames
|
||||
|
||||
self.avatarView.image = OWSAvatarBuilder.buildImage(thread: thread, diameter: kStandardAvatarSize)
|
||||
// self.avatarView.image = OWSAvatarBuilder.buildImage(thread: thread, diameter: kStandardAvatarSize)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,271 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Social
|
||||
import ContactsUI
|
||||
import MessageUI
|
||||
import SignalUtilitiesKit
|
||||
|
||||
@objc(OWSInviteFlow)
|
||||
class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailComposeViewControllerDelegate, ContactsPickerDelegate {
|
||||
enum Channel {
|
||||
case message, mail, twitter
|
||||
}
|
||||
|
||||
let installUrl = "https://signal.org/install/"
|
||||
let homepageUrl = "https://signal.org"
|
||||
|
||||
@objc
|
||||
let actionSheetController: UIAlertController
|
||||
|
||||
@objc
|
||||
let presentingViewController: UIViewController
|
||||
|
||||
var channel: Channel?
|
||||
|
||||
@objc
|
||||
required init(presentingViewController: UIViewController) {
|
||||
self.presentingViewController = presentingViewController
|
||||
actionSheetController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
super.init()
|
||||
|
||||
actionSheetController.addAction(dismissAction())
|
||||
|
||||
if let messageAction = messageAction() {
|
||||
actionSheetController.addAction(messageAction)
|
||||
}
|
||||
|
||||
if let mailAction = mailAction() {
|
||||
actionSheetController.addAction(mailAction)
|
||||
}
|
||||
|
||||
if let tweetAction = tweetAction() {
|
||||
actionSheetController.addAction(tweetAction)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
Logger.verbose("[InviteFlow] deinit")
|
||||
}
|
||||
|
||||
// MARK: Twitter
|
||||
|
||||
func canTweet() -> Bool {
|
||||
return SLComposeViewController.isAvailable(forServiceType: SLServiceTypeTwitter)
|
||||
}
|
||||
|
||||
func tweetAction() -> UIAlertAction? {
|
||||
guard canTweet() else {
|
||||
Logger.info("Twitter not supported.")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let twitterViewController = SLComposeViewController(forServiceType: SLServiceTypeTwitter) else {
|
||||
Logger.error("unable to build twitter controller.")
|
||||
return nil
|
||||
}
|
||||
|
||||
let tweetString = NSLocalizedString("SETTINGS_INVITE_TWITTER_TEXT", comment: "content of tweet when inviting via twitter - please do not translate URL")
|
||||
twitterViewController.setInitialText(tweetString)
|
||||
|
||||
let tweetUrl = URL(string: installUrl)
|
||||
twitterViewController.add(tweetUrl)
|
||||
twitterViewController.add(#imageLiteral(resourceName: "twitter_sharing_image"))
|
||||
|
||||
let tweetTitle = NSLocalizedString("SHARE_ACTION_TWEET", comment: "action sheet item")
|
||||
return UIAlertAction(title: tweetTitle, style: .default) { _ in
|
||||
Logger.debug("Chose tweet")
|
||||
|
||||
self.presentingViewController.present(twitterViewController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func dismissAction() -> UIAlertAction {
|
||||
return UIAlertAction(title: CommonStrings.dismissButton, style: .cancel)
|
||||
}
|
||||
|
||||
// MARK: ContactsPickerDelegate
|
||||
|
||||
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact]) {
|
||||
Logger.debug("didSelectContacts:\(contacts)")
|
||||
|
||||
guard let inviteChannel = channel else {
|
||||
Logger.error("unexpected nil channel after returning from contact picker.")
|
||||
self.presentingViewController.dismiss(animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
switch inviteChannel {
|
||||
case .message:
|
||||
let phoneNumbers: [String] = contacts.map { $0.userTextPhoneNumbers.first }.filter { $0 != nil }.map { $0! }
|
||||
dismissAndSendSMSTo(phoneNumbers: phoneNumbers)
|
||||
case .mail:
|
||||
let recipients: [String] = contacts.map { $0.emails.first }.filter { $0 != nil }.map { $0! }
|
||||
sendMailTo(emails: recipients)
|
||||
default:
|
||||
Logger.error("unexpected channel after returning from contact picker: \(inviteChannel)")
|
||||
}
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool {
|
||||
guard let inviteChannel = channel else {
|
||||
Logger.error("unexpected nil channel in contact picker.")
|
||||
return true
|
||||
}
|
||||
|
||||
switch inviteChannel {
|
||||
case .message:
|
||||
return contact.userTextPhoneNumbers.count > 0
|
||||
case .mail:
|
||||
return contact.emails.count > 0
|
||||
default:
|
||||
Logger.error("unexpected channel after returning from contact picker: \(inviteChannel)")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError) {
|
||||
Logger.error("with error: \(error)")
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
OWSAlerts.showErrorAlert(message: NSLocalizedString("ERROR_COULD_NOT_FETCH_CONTACTS", comment: "Error indicating that the phone's contacts could not be retrieved."))
|
||||
}
|
||||
}
|
||||
|
||||
func contactsPickerDidCancel(_: ContactsPicker) {
|
||||
Logger.debug("")
|
||||
self.presentingViewController.dismiss(animated: true)
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact) {
|
||||
owsFailDebug("InviteFlow only supports multi-select")
|
||||
self.presentingViewController.dismiss(animated: true)
|
||||
}
|
||||
|
||||
// MARK: SMS
|
||||
|
||||
func messageAction() -> UIAlertAction? {
|
||||
guard MFMessageComposeViewController.canSendText() else {
|
||||
Logger.info("Device cannot send text")
|
||||
return nil
|
||||
}
|
||||
|
||||
let messageTitle = NSLocalizedString("SHARE_ACTION_MESSAGE", comment: "action sheet item to open native messages app")
|
||||
return UIAlertAction(title: messageTitle, style: .default) { _ in
|
||||
Logger.debug("Chose message.")
|
||||
self.channel = .message
|
||||
let picker = ContactsPicker(allowsMultipleSelection: true, subtitleCellType: .phoneNumber)
|
||||
picker.contactsPickerDelegate = self
|
||||
picker.title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
|
||||
let navigationController = OWSNavigationController(rootViewController: picker)
|
||||
self.presentingViewController.present(navigationController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
public func dismissAndSendSMSTo(phoneNumbers: [String]) {
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
if phoneNumbers.count > 1 {
|
||||
let warning = UIAlertController(title: nil,
|
||||
message: NSLocalizedString("INVITE_WARNING_MULTIPLE_INVITES_BY_TEXT",
|
||||
comment: "Alert warning that sending an invite to multiple users will create a group message whose recipients will be able to see each other."),
|
||||
preferredStyle: .alert)
|
||||
warning.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_CONTINUE",
|
||||
comment: "Label for 'continue' button."),
|
||||
style: .default, handler: { _ in
|
||||
self.sendSMSTo(phoneNumbers: phoneNumbers)
|
||||
}))
|
||||
warning.addAction(OWSAlerts.cancelAction)
|
||||
self.presentingViewController.presentAlert(warning)
|
||||
} else {
|
||||
self.sendSMSTo(phoneNumbers: phoneNumbers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public func sendSMSTo(phoneNumbers: [String]) {
|
||||
let messageComposeViewController = MFMessageComposeViewController()
|
||||
messageComposeViewController.messageComposeDelegate = self
|
||||
messageComposeViewController.recipients = phoneNumbers
|
||||
|
||||
let inviteText = NSLocalizedString("SMS_INVITE_BODY", comment: "body sent to contacts when inviting to Install Signal")
|
||||
messageComposeViewController.body = inviteText.appending(" \(self.installUrl)")
|
||||
self.presentingViewController.present(messageComposeViewController, animated: true)
|
||||
}
|
||||
|
||||
// MARK: MessageComposeViewControllerDelegate
|
||||
|
||||
func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
switch result {
|
||||
case .failed:
|
||||
let warning = UIAlertController(title: nil, message: NSLocalizedString("SEND_INVITE_FAILURE", comment: "Alert body after invite failed"), preferredStyle: .alert)
|
||||
warning.addAction(UIAlertAction(title: CommonStrings.dismissButton, style: .default, handler: nil))
|
||||
self.presentingViewController.present(warning, animated: true, completion: nil)
|
||||
case .sent:
|
||||
Logger.debug("user successfully invited their friends via SMS.")
|
||||
case .cancelled:
|
||||
Logger.debug("user cancelled message invite")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Mail
|
||||
|
||||
func mailAction() -> UIAlertAction? {
|
||||
guard MFMailComposeViewController.canSendMail() else {
|
||||
Logger.info("Device cannot send mail")
|
||||
return nil
|
||||
}
|
||||
|
||||
let mailActionTitle = NSLocalizedString("SHARE_ACTION_MAIL", comment: "action sheet item to open native mail app")
|
||||
return UIAlertAction(title: mailActionTitle, style: .default) { _ in
|
||||
Logger.debug("Chose mail.")
|
||||
self.channel = .mail
|
||||
|
||||
let picker = ContactsPicker(allowsMultipleSelection: true, subtitleCellType: .email)
|
||||
picker.contactsPickerDelegate = self
|
||||
picker.title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
|
||||
let navigationController = OWSNavigationController(rootViewController: picker)
|
||||
self.presentingViewController.present(navigationController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func sendMailTo(emails recipientEmails: [String]) {
|
||||
let mailComposeViewController = MFMailComposeViewController()
|
||||
mailComposeViewController.mailComposeDelegate = self
|
||||
mailComposeViewController.setBccRecipients(recipientEmails)
|
||||
|
||||
let subject = NSLocalizedString("EMAIL_INVITE_SUBJECT", comment: "subject of email sent to contacts when inviting to install Signal")
|
||||
let bodyFormat = NSLocalizedString("EMAIL_INVITE_BODY", comment: "body of email sent to contacts when inviting to install Signal. Embeds {{link to install Signal}} and {{link to the Signal home page}}")
|
||||
let body = String.init(format: bodyFormat, installUrl, homepageUrl)
|
||||
mailComposeViewController.setSubject(subject)
|
||||
mailComposeViewController.setMessageBody(body, isHTML: false)
|
||||
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
self.presentingViewController.present(mailComposeViewController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: MailComposeViewControllerDelegate
|
||||
|
||||
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
switch result {
|
||||
case .failed:
|
||||
let warning = UIAlertController(title: nil, message: NSLocalizedString("SEND_INVITE_FAILURE", comment: "Alert body after invite failed"), preferredStyle: .alert)
|
||||
warning.addAction(UIAlertAction(title: CommonStrings.dismissButton, style: .default, handler: nil))
|
||||
self.presentingViewController.present(warning, animated: true, completion: nil)
|
||||
case .sent:
|
||||
Logger.debug("user successfully invited their friends via mail.")
|
||||
case .saved:
|
||||
Logger.debug("user saved mail invite.")
|
||||
case .cancelled:
|
||||
Logger.debug("user cancelled mail invite.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -18,30 +18,6 @@ struct LegacyNotificationConfig {
|
|||
|
||||
static func notificationAction(_ action: AppNotificationAction) -> UIUserNotificationAction {
|
||||
switch action {
|
||||
// case .answerCall:
|
||||
// let mutableAction = UIMutableUserNotificationAction()
|
||||
// mutableAction.identifier = action.identifier
|
||||
// mutableAction.title = CallStrings.answerCallButtonTitle
|
||||
// mutableAction.activationMode = .foreground
|
||||
// mutableAction.isDestructive = false
|
||||
// mutableAction.isAuthenticationRequired = false
|
||||
// return mutableAction
|
||||
// case .callBack:
|
||||
// let mutableAction = UIMutableUserNotificationAction()
|
||||
// mutableAction.identifier = action.identifier
|
||||
// mutableAction.title = CallStrings.callBackButtonTitle
|
||||
// mutableAction.activationMode = .foreground
|
||||
// mutableAction.isDestructive = false
|
||||
// mutableAction.isAuthenticationRequired = true
|
||||
// return mutableAction
|
||||
// case .declineCall:
|
||||
// let mutableAction = UIMutableUserNotificationAction()
|
||||
// mutableAction.identifier = action.identifier
|
||||
// mutableAction.title = CallStrings.declineCallButtonTitle
|
||||
// mutableAction.activationMode = .background
|
||||
// mutableAction.isDestructive = false
|
||||
// mutableAction.isAuthenticationRequired = false
|
||||
// return mutableAction
|
||||
case .markAsRead:
|
||||
let mutableAction = UIMutableUserNotificationAction()
|
||||
mutableAction.identifier = action.identifier
|
||||
|
@ -176,7 +152,7 @@ extension LegacyNotificationPresenterAdaptee: NotificationPresenterAdaptee {
|
|||
}
|
||||
|
||||
let checkForCancel = category == .incomingMessage
|
||||
if checkForCancel && hasReceivedSyncMessageRecently {
|
||||
if checkForCancel {
|
||||
assert(userInfo[AppNotificationUserInfoKey.threadId] != nil)
|
||||
notification.fireDate = Date(timeIntervalSinceNow: kNotificationDelayForRemoteRead)
|
||||
notification.timeZone = NSTimeZone.local
|
||||
|
@ -283,12 +259,6 @@ public class LegacyNotificationActionHandler: NSObject {
|
|||
}
|
||||
|
||||
switch action {
|
||||
// case .answerCall:
|
||||
// return try actionHandler.answerCall(userInfo: userInfo)
|
||||
// case .callBack:
|
||||
// return try actionHandler.callBack(userInfo: userInfo)
|
||||
// case .declineCall:
|
||||
// return try actionHandler.declineCall(userInfo: userInfo)
|
||||
case .markAsRead:
|
||||
return try actionHandler.markAsRead(userInfo: userInfo)
|
||||
case .reply:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/AppContext.h>
|
||||
#import <SessionUtilitiesKit/AppContext.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
#import "MainAppContext.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalCoreKit/Threading.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSProfileManager.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/OWSIdentityManager.h>
|
||||
#import <SessionMessagingKit/OWSIdentityManager.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
#import <MediaPlayer/MPMoviePlayerViewController.h>
|
||||
#import <MediaPlayer/MediaPlayer.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/NSData+Image.h>
|
||||
#import <SessionUtilitiesKit/NSData+Image.h>
|
||||
#import <YYImage/YYImage.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
|
|
@ -377,7 +377,6 @@ class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDel
|
|||
}
|
||||
|
||||
guard let initialDetailItem = galleryItem else {
|
||||
owsFailDebug("unexpectedly failed to build initialDetailItem.")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -587,7 +587,6 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
let conversationStyle = ConversationStyle(thread: thread)
|
||||
fetchedItem = ConversationInteractionViewItem(interaction: message,
|
||||
isGroupThread: thread.isGroupThread(),
|
||||
isRSSFeed: false,
|
||||
transaction: transaction,
|
||||
conversationStyle: conversationStyle)
|
||||
}
|
||||
|
@ -670,19 +669,12 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
|
||||
// MARK: Dynamic Header
|
||||
|
||||
private var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
private func senderName(message: TSMessage) -> String {
|
||||
switch message {
|
||||
case let incomingMessage as TSIncomingMessage:
|
||||
let hexEncodedPublicKey = incomingMessage.authorId
|
||||
if incomingMessage.thread.isGroupThread() {
|
||||
var publicChat: OpenGroup?
|
||||
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
|
||||
publicChat = LokiDatabaseUtilities.getPublicChat(for: incomingMessage.thread.uniqueId!, in: transaction)
|
||||
}
|
||||
let publicChat = Storage.shared.getOpenGroup(for: incomingMessage.thread.uniqueId!)
|
||||
if let publicChat = publicChat {
|
||||
return UserDisplayNameUtilities.getPublicChatDisplayName(for: hexEncodedPublicKey, in: publicChat.channel, on: publicChat.server) ?? hexEncodedPublicKey
|
||||
} else {
|
||||
|
|
|
@ -13,23 +13,21 @@ protocol MessageActionsDelegate: class {
|
|||
}
|
||||
|
||||
struct MessageActionBuilder {
|
||||
|
||||
static func reply(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction {
|
||||
return MenuAction(image: #imageLiteral(resourceName: "ic_reply"),
|
||||
title: NSLocalizedString("MESSAGE_ACTION_REPLY", comment: "Action sheet button title"),
|
||||
subtitle: nil,
|
||||
block: { [weak delegate] (_) in
|
||||
delegate?.messageActionsReplyToItem(conversationViewItem)
|
||||
|
||||
})
|
||||
block: { [weak delegate] _ in delegate?.messageActionsReplyToItem(conversationViewItem) }
|
||||
)
|
||||
}
|
||||
|
||||
static func copyText(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction {
|
||||
return MenuAction(image: #imageLiteral(resourceName: "ic_copy"),
|
||||
title: NSLocalizedString("MESSAGE_ACTION_COPY_TEXT", comment: "Action sheet button title"),
|
||||
subtitle: nil,
|
||||
block: { (_) in
|
||||
conversationViewItem.copyTextAction()
|
||||
})
|
||||
block: { _ in conversationViewItem.copyTextAction() }
|
||||
)
|
||||
}
|
||||
|
||||
static func copyPublicKey(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction {
|
||||
|
@ -44,9 +42,8 @@ struct MessageActionBuilder {
|
|||
return MenuAction(image: #imageLiteral(resourceName: "ic_info"),
|
||||
title: NSLocalizedString("MESSAGE_ACTION_DETAILS", comment: "Action sheet button title"),
|
||||
subtitle: nil,
|
||||
block: { [weak delegate] (_) in
|
||||
delegate?.messageActionsShowDetailsForItem(conversationViewItem)
|
||||
})
|
||||
block: { [weak delegate] _ in delegate?.messageActionsShowDetailsForItem(conversationViewItem) }
|
||||
)
|
||||
}
|
||||
|
||||
static func report(_ conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction {
|
||||
|
@ -61,27 +58,24 @@ struct MessageActionBuilder {
|
|||
return MenuAction(image: #imageLiteral(resourceName: "ic_trash"),
|
||||
title: NSLocalizedString("MESSAGE_ACTION_DELETE_MESSAGE", comment: "Action sheet button title"),
|
||||
subtitle: nil,
|
||||
block: { (_) in
|
||||
conversationViewItem.deleteAction()
|
||||
})
|
||||
block: { _ in conversationViewItem.deleteAction() }
|
||||
)
|
||||
}
|
||||
|
||||
static func copyMedia(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction {
|
||||
return MenuAction(image: #imageLiteral(resourceName: "ic_copy"),
|
||||
title: NSLocalizedString("MESSAGE_ACTION_COPY_MEDIA", comment: "Action sheet button title"),
|
||||
subtitle: nil,
|
||||
block: { (_) in
|
||||
conversationViewItem.copyMediaAction()
|
||||
})
|
||||
block: { _ in conversationViewItem.copyMediaAction() }
|
||||
)
|
||||
}
|
||||
|
||||
static func saveMedia(conversationViewItem: ConversationViewItem, delegate: MessageActionsDelegate) -> MenuAction {
|
||||
return MenuAction(image: #imageLiteral(resourceName: "ic_download"),
|
||||
title: NSLocalizedString("MESSAGE_ACTION_SAVE_MEDIA", comment: "Action sheet button title"),
|
||||
subtitle: nil,
|
||||
block: { (_) in
|
||||
conversationViewItem.saveMediaAction()
|
||||
})
|
||||
block: { _ in conversationViewItem.saveMediaAction() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,9 +87,8 @@ class ConversationViewItemActions: NSObject {
|
|||
var actions: [MenuAction] = []
|
||||
|
||||
let isGroup = conversationViewItem.isGroupThread;
|
||||
let isRSSFeed = conversationViewItem.isRSSFeed;
|
||||
|
||||
if shouldAllowReply && !isRSSFeed {
|
||||
if shouldAllowReply {
|
||||
let replyAction = MessageActionBuilder.reply(conversationViewItem: conversationViewItem, delegate: delegate)
|
||||
actions.append(replyAction)
|
||||
}
|
||||
|
@ -105,7 +98,7 @@ class ConversationViewItemActions: NSObject {
|
|||
actions.append(copyTextAction)
|
||||
}
|
||||
|
||||
if isGroup && !isRSSFeed && conversationViewItem.interaction is TSIncomingMessage {
|
||||
if isGroup && conversationViewItem.interaction is TSIncomingMessage {
|
||||
let copyPublicKeyAction = MessageActionBuilder.copyPublicKey(conversationViewItem: conversationViewItem, delegate: delegate)
|
||||
actions.append(copyPublicKeyAction)
|
||||
}
|
||||
|
@ -132,9 +125,8 @@ class ConversationViewItemActions: NSObject {
|
|||
var actions: [MenuAction] = []
|
||||
|
||||
let isGroup = conversationViewItem.isGroupThread;
|
||||
let isRSSFeed = conversationViewItem.isRSSFeed;
|
||||
|
||||
if shouldAllowReply && !isRSSFeed {
|
||||
if shouldAllowReply {
|
||||
let replyAction = MessageActionBuilder.reply(conversationViewItem: conversationViewItem, delegate: delegate)
|
||||
actions.append(replyAction)
|
||||
}
|
||||
|
@ -150,7 +142,7 @@ class ConversationViewItemActions: NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
if isGroup && !isRSSFeed && conversationViewItem.interaction is TSIncomingMessage {
|
||||
if isGroup && conversationViewItem.interaction is TSIncomingMessage {
|
||||
let copyPublicKeyAction = MessageActionBuilder.copyPublicKey(conversationViewItem: conversationViewItem, delegate: delegate)
|
||||
actions.append(copyPublicKeyAction)
|
||||
}
|
||||
|
@ -182,9 +174,8 @@ class ConversationViewItemActions: NSObject {
|
|||
}
|
||||
|
||||
let isGroup = conversationViewItem.isGroupThread;
|
||||
let isRSSFeed = conversationViewItem.isRSSFeed;
|
||||
|
||||
if isGroup && !isRSSFeed && conversationViewItem.interaction is TSIncomingMessage {
|
||||
if isGroup && conversationViewItem.interaction is TSIncomingMessage {
|
||||
let copyPublicKeyAction = MessageActionBuilder.copyPublicKey(conversationViewItem: conversationViewItem, delegate: delegate)
|
||||
actions.append(copyPublicKeyAction)
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ protocol MessageDetailViewDelegate: AnyObject {
|
|||
}
|
||||
|
||||
@objc
|
||||
class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDelegate, OWSMessageBubbleViewDelegate, ContactShareViewHelperDelegate {
|
||||
class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDelegate, OWSMessageBubbleViewDelegate {
|
||||
|
||||
@objc
|
||||
weak var delegate: MessageDetailViewDelegate?
|
||||
|
@ -52,18 +52,12 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
|
|||
|
||||
var conversationStyle: ConversationStyle
|
||||
|
||||
private var contactShareViewHelper: ContactShareViewHelper!
|
||||
|
||||
// MARK: Dependencies
|
||||
|
||||
var preferences: OWSPreferences {
|
||||
return Environment.shared.preferences
|
||||
}
|
||||
|
||||
var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
@available(*, unavailable, message:"use other constructor instead.")
|
||||
|
@ -86,8 +80,6 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
|
|||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.contactShareViewHelper = ContactShareViewHelper(contactsManager: contactsManager)
|
||||
contactShareViewHelper.delegate = self
|
||||
|
||||
do {
|
||||
try updateMessageToLatest()
|
||||
|
@ -554,8 +546,6 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
|
|||
|
||||
private func string(for messageReceiptStatus: MessageReceiptStatus) -> String {
|
||||
switch messageReceiptStatus {
|
||||
case .calculatingPoW:
|
||||
return NSLocalizedString("Calculating proof of work", comment: "")
|
||||
case .uploading:
|
||||
return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_UPLOADING",
|
||||
comment: "Status label for messages which are uploading.")
|
||||
|
@ -614,27 +604,6 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
|
|||
mediaGallery.presentDetailView(fromViewController: self, mediaAttachment: attachmentStream, replacingView: imageView)
|
||||
}
|
||||
|
||||
func didTapContactShare(_ viewItem: ConversationViewItem) {
|
||||
guard let contactShare = viewItem.contactShare else {
|
||||
owsFailDebug("missing contact.")
|
||||
return
|
||||
}
|
||||
let contactViewController = ContactViewController(contactShare: contactShare)
|
||||
self.navigationController?.pushViewController(contactViewController, animated: true)
|
||||
}
|
||||
|
||||
func didTapSendMessage(toContactShare contactShare: ContactShareViewModel) {
|
||||
contactShareViewHelper.sendMessage(contactShare: contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didTapSendInvite(toContactShare contactShare: ContactShareViewModel) {
|
||||
contactShareViewHelper.showInviteContact(contactShare: contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didTapShowAddToContactUI(forContactShare contactShare: ContactShareViewModel) {
|
||||
contactShareViewHelper.showAddToContacts(contactShare: contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
var audioAttachmentPlayer: OWSAudioPlayer?
|
||||
|
||||
func didTapAudioViewItem(_ viewItem: ConversationViewItem, attachmentStream: TSAttachmentStream) {
|
||||
|
|
|
@ -1,175 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
@objc(OWSMessageFetcherJob)
|
||||
public class MessageFetcherJob: NSObject {
|
||||
|
||||
private var timer: Timer?
|
||||
|
||||
@objc
|
||||
public override init() {
|
||||
super.init()
|
||||
|
||||
SwiftSingletons.register(self)
|
||||
}
|
||||
|
||||
// MARK: Singletons
|
||||
|
||||
private var networkManager: TSNetworkManager {
|
||||
return SSKEnvironment.shared.networkManager
|
||||
}
|
||||
|
||||
private var messageReceiver: OWSMessageReceiver {
|
||||
return SSKEnvironment.shared.messageReceiver
|
||||
}
|
||||
|
||||
private var signalService: OWSSignalService {
|
||||
return OWSSignalService.sharedInstance()
|
||||
}
|
||||
|
||||
// MARK:
|
||||
|
||||
@discardableResult
|
||||
public func run() -> Promise<Void> {
|
||||
let promise = fetchUndeliveredMessages().then { promises -> Promise<Void> in
|
||||
let promises = promises.map { promise -> Promise<Void> in
|
||||
return promise.then { envelopes -> Promise<Void> in
|
||||
for envelope in envelopes {
|
||||
Logger.info("Envelope received.")
|
||||
do {
|
||||
let envelopeData = try envelope.serializedData()
|
||||
self.messageReceiver.handleReceivedEnvelopeData(envelopeData)
|
||||
} catch {
|
||||
owsFailDebug("Failed to serialize envelope.")
|
||||
}
|
||||
self.acknowledgeDelivery(envelope: envelope)
|
||||
}
|
||||
return Promise.value(())
|
||||
}
|
||||
}
|
||||
return when(resolved: promises).asVoid()
|
||||
}
|
||||
promise.retainUntilComplete()
|
||||
return promise
|
||||
}
|
||||
|
||||
@objc
|
||||
@discardableResult
|
||||
public func run() -> AnyPromise {
|
||||
return AnyPromise(run() as Promise)
|
||||
}
|
||||
|
||||
// use in DEBUG or wherever you can't receive push notifications to poll for messages.
|
||||
// Do not use in production.
|
||||
public func startRunLoop(timeInterval: Double) {
|
||||
Logger.error("Starting message fetch polling. This should not be used in production.")
|
||||
timer = WeakTimer.scheduledTimer(timeInterval: timeInterval, target: self, userInfo: nil, repeats: true) {[weak self] _ in
|
||||
let _: Promise<Void>? = self?.run()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
public func stopRunLoop() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
}
|
||||
|
||||
private func parseMessagesResponse(responseObject: Any?) -> (envelopes: [SSKProtoEnvelope], more: Bool)? {
|
||||
guard let responseObject = responseObject else {
|
||||
Logger.error("response object was surpringly nil")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let responseDict = responseObject as? [String: Any] else {
|
||||
Logger.error("response object was not a dictionary")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let messageDicts = responseDict["messages"] as? [[String: Any]] else {
|
||||
Logger.error("messages object was not a list of dictionaries")
|
||||
return nil
|
||||
}
|
||||
|
||||
let moreMessages = { () -> Bool in
|
||||
if let responseMore = responseDict["more"] as? Bool {
|
||||
return responseMore
|
||||
} else {
|
||||
Logger.warn("more object was not a bool. Assuming no more")
|
||||
return false
|
||||
}
|
||||
}()
|
||||
|
||||
let envelopes: [SSKProtoEnvelope] = messageDicts.compactMap { buildEnvelope(messageDict: $0) }
|
||||
|
||||
return (
|
||||
envelopes: envelopes,
|
||||
more: moreMessages
|
||||
)
|
||||
}
|
||||
|
||||
private func buildEnvelope(messageDict: [String: Any]) -> SSKProtoEnvelope? {
|
||||
do {
|
||||
let params = ParamParser(dictionary: messageDict)
|
||||
|
||||
let typeInt: Int32 = try params.required(key: "type")
|
||||
guard let type: SSKProtoEnvelope.SSKProtoEnvelopeType = SSKProtoEnvelope.SSKProtoEnvelopeType(rawValue: typeInt) else {
|
||||
Logger.error("`type` was invalid: \(typeInt)")
|
||||
throw ParamParser.ParseError.invalidFormat("type")
|
||||
}
|
||||
|
||||
guard let timestamp: UInt64 = try params.required(key: "timestamp") else {
|
||||
Logger.error("`timestamp` was invalid: \(typeInt)")
|
||||
throw ParamParser.ParseError.invalidFormat("timestamp")
|
||||
}
|
||||
|
||||
let builder = SSKProtoEnvelope.builder(type: type, timestamp: timestamp)
|
||||
|
||||
if let source: String = try params.optional(key: "source") {
|
||||
builder.setSource(source)
|
||||
}
|
||||
|
||||
if let sourceDevice: UInt32 = try params.optional(key: "sourceDevice") {
|
||||
builder.setSourceDevice(sourceDevice)
|
||||
}
|
||||
|
||||
if let legacyMessage = try params.optionalBase64EncodedData(key: "message") {
|
||||
builder.setLegacyMessage(legacyMessage)
|
||||
}
|
||||
if let content = try params.optionalBase64EncodedData(key: "content") {
|
||||
builder.setContent(content)
|
||||
}
|
||||
if let serverTimestamp: UInt64 = try params.optional(key: "serverTimestamp") {
|
||||
builder.setServerTimestamp(serverTimestamp)
|
||||
}
|
||||
if let serverGuid: String = try params.optional(key: "guid") {
|
||||
builder.setServerGuid(serverGuid)
|
||||
}
|
||||
|
||||
return try builder.build()
|
||||
} catch {
|
||||
owsFailDebug("error building envelope: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchUndeliveredMessages() -> Promise<Set<Promise<[SSKProtoEnvelope]>>> {
|
||||
let userPublickKey = getUserHexEncodedPublicKey() // Can be missing in rare cases
|
||||
guard !userPublickKey.isEmpty else { return Promise.value(Set()) }
|
||||
return SnodeAPI.getMessages(for: userPublickKey).map2 { promises -> Set<Promise<[SSKProtoEnvelope]>> in
|
||||
return Set(promises.map { promise -> Promise<[SSKProtoEnvelope]> in
|
||||
return promise.map2 { rawMessages -> [SSKProtoEnvelope] in
|
||||
return rawMessages.compactMap { SSKProtoEnvelope.from($0) }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func acknowledgeDelivery(envelope: SSKProtoEnvelope) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
|
@ -14,7 +14,6 @@ import SignalUtilitiesKit
|
|||
case read
|
||||
case failed
|
||||
case skipped
|
||||
case calculatingPoW
|
||||
}
|
||||
|
||||
@objc
|
||||
|
@ -110,10 +109,6 @@ public class MessageRecipientStatusUtils: NSObject {
|
|||
// Use the "long" version of this message here.
|
||||
return (.failed, NSLocalizedString("MESSAGE_STATUS_FAILED", comment: "status message for failed messages"))
|
||||
case .sending:
|
||||
if outgoingMessage.isCalculatingPoW {
|
||||
return (.calculatingPoW, NSLocalizedString("Calculating proof of work", comment: ""))
|
||||
}
|
||||
|
||||
if outgoingMessage.hasAttachments() {
|
||||
return (.uploading, NSLocalizedString("MESSAGE_STATUS_UPLOADING",
|
||||
comment: "status message while attachment is uploading"))
|
||||
|
@ -169,8 +164,6 @@ public class MessageRecipientStatusUtils: NSObject {
|
|||
return "failed"
|
||||
case .skipped:
|
||||
return "skipped"
|
||||
case .calculatingPoW:
|
||||
return "calculatingPoW"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,185 +0,0 @@
|
|||
////
|
||||
//// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
////
|
||||
//
|
||||
//import Foundation
|
||||
//import SignalUtilitiesKit
|
||||
//import SignalUtilitiesKit
|
||||
//
|
||||
///**
|
||||
// * Manage call related UI in a pre-CallKit world.
|
||||
// */
|
||||
//class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee {
|
||||
//
|
||||
// let notificationPresenter: NotificationPresenter
|
||||
// let callService: CallService
|
||||
//
|
||||
// // Starting/Stopping incoming call ringing is our apps responsibility for the non CallKit interface.
|
||||
// let hasManualRinger = true
|
||||
//
|
||||
// required init(callService: CallService, notificationPresenter: NotificationPresenter) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// self.callService = callService
|
||||
// self.notificationPresenter = notificationPresenter
|
||||
//
|
||||
// super.init()
|
||||
// }
|
||||
//
|
||||
// // MARK: Dependencies
|
||||
//
|
||||
// var audioSession: OWSAudioSession {
|
||||
// return Environment.shared.audioSession
|
||||
// }
|
||||
//
|
||||
// // MARK:
|
||||
//
|
||||
// func startOutgoingCall(handle: String) -> SignalCall {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// let call = SignalCall.outgoingCall(localId: UUID(), remotePhoneNumber: handle)
|
||||
//
|
||||
// // make sure we don't terminate audio session during call
|
||||
// let success = self.audioSession.startAudioActivity(call.audioActivity)
|
||||
// assert(success)
|
||||
//
|
||||
// self.callService.handleOutgoingCall(call).retainUntilComplete()
|
||||
//
|
||||
// return call
|
||||
// }
|
||||
//
|
||||
// func reportIncomingCall(_ call: SignalCall, callerName: String) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.debug("")
|
||||
//
|
||||
// self.showCall(call)
|
||||
//
|
||||
// // present lock screen notification
|
||||
// if UIApplication.shared.applicationState == .active {
|
||||
// Logger.debug("skipping notification since app is already active.")
|
||||
// } else {
|
||||
// notificationPresenter.presentIncomingCall(call, callerName: callerName)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func reportMissedCall(_ call: SignalCall, callerName: String) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// notificationPresenter.presentMissedCall(call, callerName: callerName)
|
||||
// }
|
||||
//
|
||||
// func answerCall(localId: UUID) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// guard let call = self.callService.call else {
|
||||
// owsFailDebug("No current call.")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// guard call.localId == localId else {
|
||||
// owsFailDebug("localId does not match current call")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.answerCall(call)
|
||||
// }
|
||||
//
|
||||
// func answerCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// guard call.localId == self.callService.call?.localId else {
|
||||
// owsFailDebug("localId does not match current call")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.audioSession.isRTCAudioEnabled = true
|
||||
// self.callService.handleAnswerCall(call)
|
||||
// }
|
||||
//
|
||||
// func declineCall(localId: UUID) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// guard let call = self.callService.call else {
|
||||
// owsFailDebug("No current call.")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// guard call.localId == localId else {
|
||||
// owsFailDebug("localId does not match current call")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.declineCall(call)
|
||||
// }
|
||||
//
|
||||
// func declineCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// guard call.localId == self.callService.call?.localId else {
|
||||
// owsFailDebug("localId does not match current call")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.callService.handleDeclineCall(call)
|
||||
// }
|
||||
//
|
||||
// func recipientAcceptedCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// self.audioSession.isRTCAudioEnabled = true
|
||||
// }
|
||||
//
|
||||
// func localHangupCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// // If both parties hang up at the same moment,
|
||||
// // call might already be nil.
|
||||
// guard self.callService.call == nil || call.localId == self.callService.call?.localId else {
|
||||
// owsFailDebug("localId does not match current call")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.callService.handleLocalHungupCall(call)
|
||||
// }
|
||||
//
|
||||
// internal func remoteDidHangupCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.debug("is no-op")
|
||||
// }
|
||||
//
|
||||
// internal func remoteBusy(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.debug("is no-op")
|
||||
// }
|
||||
//
|
||||
// internal func failCall(_ call: SignalCall, error: CallError) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.debug("is no-op")
|
||||
// }
|
||||
//
|
||||
// func setIsMuted(call: SignalCall, isMuted: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// guard call.localId == self.callService.call?.localId else {
|
||||
// owsFailDebug("localId does not match current call")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.callService.setIsMuted(call: call, isMuted: isMuted)
|
||||
// }
|
||||
//
|
||||
// func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// guard call.localId == self.callService.call?.localId else {
|
||||
// owsFailDebug("localId does not match current call")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.callService.setHasLocalVideo(hasLocalVideo: hasLocalVideo)
|
||||
// }
|
||||
//}
|
|
@ -5,7 +5,7 @@
|
|||
#import "NotificationSettingsOptionsViewController.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "SignalApp.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
||||
@implementation NotificationSettingsOptionsViewController
|
||||
|
@ -63,9 +63,6 @@
|
|||
{
|
||||
[Environment.shared.preferences setNotificationPreviewType:notificationType];
|
||||
|
||||
// rebuild callUIAdapter since notification configuration changed.
|
||||
// [AppEnvironment.shared.callService createCallUIAdapter];
|
||||
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
#import "NotificationSettingsViewController.h"
|
||||
#import "NotificationSettingsOptionsViewController.h"
|
||||
#import "OWSSoundSettingsViewController.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSPreferences.h>
|
||||
#import <SignalUtilitiesKit/OWSSounds.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SessionMessagingKit/OWSPreferences.h>
|
||||
#import <SessionMessagingKit/OWSSounds.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import "Session-Swift.h"
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue