Merge pull request #313 from loki-project/refactor-4

Refactoring Part 3
This commit is contained in:
Niels Andriesse 2020-11-30 13:19:03 +11:00 committed by GitHub
commit 60aa2811bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
816 changed files with 6504 additions and 75873 deletions

20
Podfile
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSTableViewController.h"
@interface AdvancedSettingsTableViewController : OWSTableViewController
@end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
*/
}
}

View File

@ -4,7 +4,7 @@
#import "OWSBubbleShapeView.h"
#import "OWSBubbleView.h"
#import <SignalUtilitiesKit/UIView+OWS.h>
#import <SessionUtilitiesKit/UIView+OWS.h>
NS_ASSUME_NONNULL_BEGIN

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
//
#import "OWSMessageTextView.h"
#import <SignalUtilitiesKit/UIView+OWS.h>
#import <SessionUtilitiesKit/UIView+OWS.h>
NS_ASSUME_NONNULL_BEGIN

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -377,7 +377,6 @@ class MediaGallery: NSObject, MediaGalleryDataSource, MediaTileViewControllerDel
}
guard let initialDetailItem = galleryItem else {
owsFailDebug("unexpectedly failed to build initialDetailItem.")
return
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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