Merge branch 'dev' into link-previews

This commit is contained in:
Niels Andriesse 2020-12-18 11:57:11 +11:00
commit 61a0672824
856 changed files with 8224 additions and 76788 deletions

View File

@ -26,9 +26,9 @@ Describe here the issue that you are experiencing.
- list the steps
- that reproduce the bug
**Actual result:** Describe here what happens after you run the steps above (i.e. the buggy behaviour)
**Actual result:** Describe here what happens after you run the steps above (i.e. the buggy behaviour).
**Expected result:** Describe here what should happen after you run the steps above (i.e. what would be the correct behaviour)
**Expected result:** Describe here what should happen after you run the steps above (i.e. what would be the correct behaviour).
### Screenshots
<!-- you can drag and drop images below -->
@ -41,27 +41,3 @@ Describe here the issue that you are experiencing.
**iOS version**: X.Y.Z
**Session version:** X.Y.Z
### Link to debug log
<!-- Ensure that "Enable Debug Log" is on in Session's settings then make the bug happen and immediately after that tap "Submit Debug Log" from settings and paste the link below. -->
<!-- If this is a crashing bug, after filing this issue, email a copy of your latest crash report to support@loki.network
To get a crash log:
1. Go to the iOS Settings app.
2. Go to Privacy.
3. Go to Analytics or Diagnostics & Usage.
4. Select Analytics Data or Diagnostics & Usage Data.
5. Locate the .ips crash log for Session.
The logs will be named in the format: Session(DateTime).ips
6. Select the desired Session log.
7.a iOS 11 users, tap the Share icon in the top right corner and jump to step 10.
7.b iOS 9&10 users, long press to see the option to highlight text and select the entire text of the log. It will end in EOF.
8. Once the text is selected, tap Copy.
9. Paste the copied text into an email.
10. Send the email to support@loki.network with a subject like:
* "iOS Crash Log: (your github issue)"
* Example subject: iOS Crash Log: Crash on launch #111
* Example subject: iOS Crash Log: Crash when sending video #222
-->

View File

@ -13,7 +13,7 @@ A clear and concise description of what the bug is.
**To reproduce**
Steps to reproduce the behavior:
Steps to reproduce the behavior.
**Screenshots or logs**

View File

@ -1,12 +1,6 @@
<!-- You can remove this first section if you have contributed before -->
### First time contributor checklist
<!-- replace the empty checkboxes [ ] below with checked ones [x] accordingly -->
- [ ] I have read the [README](https://github.com/WhisperSystems/Signal-iOS/blob/master/README.md) and [CONTRIBUTING](https://github.com/WhisperSystems/Signal-iOS/blob/master/CONTRIBUTING.md) documents
- [ ] I have signed the [Contributor Licence Agreement](https://whispersystems.org/cla/)
### Contributor checklist
<!-- replace the empty checkboxes [ ] below with checked ones [x] accordingly -->
- [ ] I'm following the [code, UI and style conventions](https://github.com/WhisperSystems/Signal-iOS/blob/master/CONTRIBUTING.md#code-conventions)
- [ ] My commits are rebased on the latest master branch
- [ ] My commits are in nice logical chunks
- [ ] My contribution is fully baked and is ready to be merged as is

View File

@ -31,12 +31,12 @@ You can then add the Session repo to sync with upstream changes:
git remote add upstream https://github.com/loki-project/session-ios
```
## 2. Dependencies
## 2. Pods
To build and configure the libraries Session uses, just run:
```
make dependencies
pod install
```
## 3. Xcode
@ -49,21 +49,23 @@ open Signal.xcworkspace
In the TARGETS area of the General tab, change the Team dropdown to
your own. You will need to do that for all the listed targets, for ex.
Signal, SignalShareExtension, and SignalMessaging. You will need an Apple
Developer account for this.
Session, SessionShareExtension, and SessionNotificationServiceExtension. You
will need an Apple Developer account for this.
On the Capabilities tab, turn off Push Notifications and Data Protection,
while keeping Background Modes on. The App Groups capability will need to
remain on in order to access the shared data storage. The App ID needs to
match the SignalApplicationGroup string set in TSConstants.h.
If you wish to test the Documents API, the iCloud capability will need to
be on with the iCloud Documents option selected.
remain on in order to access the shared data storage.
Build and Run and you are ready to go!
## Known issues
### PureLayout
The PureLayout post install hook doesn't get applied correctly upon running
`pod install` if you're on Xcode 12. See https://github.com/CocoaPods/CocoaPods/issues/10087
for more information.
### Push Notifications
Features related to push notifications are known to be not working for
third-party contributors since Apple's Push Notification service pushes
will only work with the Session production code signing

View File

@ -1,62 +0,0 @@
# Contributing to Session iOS
Thank you for supporting Session and looking for ways to help. Please note that some conventions here might be a bit different than what you are used to, even if you have contributed to other open source projects before. Reading this document will help you save time and work effectively with the developers and other contributors.
## Where do I start?
The bulk of the Session code can be found under Signal/src/Loki and SignalServiceKit/src/Loki.
## Development ideology
Truths which we believe to be self-evident:
1. **The answer is not more options.** If you feel compelled to add a preference that's exposed to the user, it's very possible you've made a wrong turn somewhere.
1. **The user doesn't know what a key is.** We need to minimize the points at which a user is exposed to this sort of terminology as extremely as possible.
1. **There are no power users.** The idea that some users "understand" concepts better than others has proven to be, for the most part, false. If anything, "power users" are more dangerous than the rest, and we should avoid exposing dangerous functionality to them.
1. **If it's "like PGP," it's wrong.** PGP is our guide for what not to do.
1. **It's an asynchronous world.** Be wary of anything that is anti-asynchronous: ACKs, protocol confirmations, or any protocol-level "advisory" message.
1. **There is no such thing as time.** Protocol ideas that require synchronized clocks are doomed to failure.
## Issues
Please search both open and closed issues to make sure your bug report is not a duplicate.
### Open issues
#### If it's open, it's tracked
The developers read every issue, but high-priority bugs or features can take precedence over others. Session is an open source project, and everyone is encouraged to play an active role in diagnosing and fixing open issues.
### Closed issues
#### "My issue was closed without giving a reason!"
Although we do our best, writing detailed explanations for every issue can be time consuming, and the topic also might have been covered previously in other related issues.
## Pull requests
### Smaller is better
Big changes are significantly less likely to be accepted. Large features often require protocol modifications and necessitate a staged rollout process that is coordinated across millions of users on multiple platforms (Android, iOS, and Desktop).
Try not to take on too much at once. As a first-time contributor, we recommend starting with small and simple PRs in order to become familiar with the codebase. Most of the work should go into discovering which three lines need to change rather than writing the code.
### Submit finished and well-tested pull requests
Please do not submit pull requests that are still a work in progress. Pull requests should be thoroughly tested and ready to merge before they are submitted.
### Merging can sometimes take a while
If your pull request follows all of the advice above but still has not been merged, this usually means that the developers haven't had time to review it yet. We understand that this might feel frustrating, and we apologize.
## How can I contribute?
There are several other ways to get involved:
* Help new users learn about Session.
* Redirect support questions to support@loki.network.
* Improve documentation in the [wiki](https://github.com/loki-project/session-protocol-docs/wiki).
* Find and mark duplicate issues.
* Try to reproduce issues and help with troubleshooting.
* Discover solutions to open issues and post any relevant findings.
* Test other people's pull requests.
* Share Session with your friends and family.
Session is made for you. Thank you for your feedback and support.

View File

@ -1,14 +0,0 @@
Apart from the general `BUILDING.md` there are certain things that have
to be done by Session maintainers.
For transparency and bus factor, they are outlined here.
## Dependencies
Keeping Cocoapods based dependencies is easy enough.
`pod update`
Similarly, Carthage dependencies can be updated like so:
`carthage update`

28
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,18 +28,13 @@ 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
pod 'AFNetworking', inhibit_warnings: true
pod 'CryptoSwift', :inhibit_warnings => true
target 'SessionNotificationServiceExtension' do
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 '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 +52,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 +64,16 @@ 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 'Sodium', :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 +92,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: 3263ab95f60e220882ca53cca4c6bdc2e7a80381
COCOAPODS: 1.9.3
COCOAPODS: 1.10.0.rc.1

View File

@ -17,6 +17,10 @@ Please search for any [existing issues](https://github.com/loki-project/session-
Build instructions can be found in [BUILDING.md](BUILDING.md).
## Translations
Want to help us translate Session into your language? See [TRANSLATION.md](TRANSLATION.md) for instructions on how to do that!
## License
Copyright 2011 Whisper Systems

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

@ -31,6 +31,14 @@ final class NewConversationButtonSet : UIView {
}
private func setUpViewHierarchy() {
mainButton.accessibilityLabel = "Toggle conversation options button"
mainButton.isAccessibilityElement = true
createNewPrivateChatButton.accessibilityLabel = "Start new one-on-one conversation button"
createNewPrivateChatButton.isAccessibilityElement = true
createNewClosedGroupButton.accessibilityLabel = "Start new closed group button"
createNewClosedGroupButton.isAccessibilityElement = true
joinOpenGroupButton.accessibilityLabel = "Join open group button"
joinOpenGroupButton.isAccessibilityElement = true
let inset = (Values.newConversationButtonExpandedSize - Values.newConversationButtonCollapsedSize) / 2
addSubview(joinOpenGroupButton)
horizontalButtonConstraints[joinOpenGroupButton] = joinOpenGroupButton.pin(.left, to: .left, of: self, withInset: inset)

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

@ -1,25 +0,0 @@
import SessionMessagingKit
import SessionProtocolKit
import SessionSnodeKit
@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
)
SessionProtocolKit.configure(storage: Storage.shared, sharedSenderKeysDelegate: UIApplication.shared.delegate as! AppDelegate)
SessionSnodeKit.configure(storage: Storage.shared)
}
}

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

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Shield.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

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];
@ -64,8 +63,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 +71,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;
@ -191,6 +166,11 @@ static NSTimeInterval launchStartedAt;
[LKAppModeManager configureWithDelegate:self];
// OWSLinkPreview and OpenGroup are now in SessionMessagingKit, so to still be able to deserialize them we
// need to tell NSKeyedUnarchiver about the changes.
[NSKeyedUnarchiver setClass:OWSLinkPreview.class forClassName:@"SessionServiceKit.OWSLinkPreview"];
[NSKeyedUnarchiver setClass:SNOpenGroup.class forClassName:@"LKPublicChat"];
BOOL isLoggingEnabled;
#ifdef DEBUG
// Specified at Product -> Scheme -> Edit Scheme -> Test -> Arguments -> Environment to avoid things like
@ -220,8 +200,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 +257,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 +366,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 +400,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
@ -619,19 +516,7 @@ static NSTimeInterval launchStartedAt;
[Environment.shared.preferences setHasGeneratedThumbnails:YES];
}];
}
#ifdef DEBUG
// A bug in orphan cleanup could be disastrous so let's only
// run it in DEBUG builds for a few releases.
//
// TODO: Release to production once we have analytics.
// TODO: Orphan cleanup is somewhat expensive - not least in doing a bunch
// TODO: of disk access. We might want to only run it "once per version"
// 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 +525,6 @@ static NSTimeInterval launchStartedAt;
[self ensureRootViewController];
[self.messageManager startObserving];
[self.udManager setup];
[self preheatDatabaseViews];
@ -657,8 +540,6 @@ static NSTimeInterval launchStartedAt;
if (appVersion.lastAppVersion.length > 0
&& ![appVersion.lastAppVersion isEqualToString:appVersion.currentAppVersion]) {
[[self.tsAccountManager updateAccountAttributes] retainUntilComplete];
[SSKEnvironment.shared.syncManager sendConfigurationSyncMessage];
}
}
}
@ -734,18 +615,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 +631,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 unregisterToken:deviceToken];
}
}
@ -782,7 +651,6 @@ static NSTimeInterval launchStartedAt;
OWSLogWarn(@"We're in debug mode. Faking success for remote registration with a fake push identifier.");
[self.pushRegistrationManager didReceiveVanillaPushToken:[[NSMutableData dataWithLength:32] copy]];
#else
OWSProdError([OWSAnalyticsEvents appDelegateErrorFailedToRegisterForRemoteNotifications]);
[self.pushRegistrationManager didFailToReceiveVanillaPushTokenWithError:error];
#endif
}
@ -901,7 +769,6 @@ static NSTimeInterval launchStartedAt;
- (void)startOpenGroupPollersIfNeeded
{
[LKPublicChatManager.shared startPollersIfNeeded];
[SSKEnvironment.shared.attachmentDownloads continueDownloadIfPossible];
}
- (void)stopOpenGroupPollers { [LKPublicChatManager.shared stopPollers]; }
@ -950,10 +817,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 unregisterToken:deviceToken] 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
@ -130,6 +127,8 @@ const CGFloat kMaxTextViewHeight = 120;
self.inputTextView.backgroundColor = LKColors.composeViewTextFieldBackground;
[self.inputTextView setContentHuggingLow];
[self.inputTextView setCompressionResistanceLow];
self.inputTextView.accessibilityLabel = @"Input text view";
self.inputTextView.isAccessibilityElement = YES;
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _inputTextView);
_textViewHeightConstraint = [self.inputTextView autoSetDimension:ALDimensionHeight toSize:kMinTextViewHeight];
@ -150,11 +149,15 @@ const CGFloat kMaxTextViewHeight = 120;
[self.sendButton autoSetDimensionsToSize:CGSizeMake(40, kMinTextViewHeight)];
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _sendButton);
[self.sendButton addTarget:self action:@selector(sendButtonPressed) forControlEvents:UIControlEventTouchUpInside];
self.sendButton.accessibilityLabel = @"Send button";
self.sendButton.isAccessibilityElement = YES;
_voiceMemoButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *voiceMemoIcon = [[UIImage imageNamed:@"Microphone"] asTintedImageWithColor:LKColors.text];
[self.voiceMemoButton setImage:voiceMemoIcon forState:UIControlStateNormal];
[self.voiceMemoButton autoSetDimensionsToSize:CGSizeMake(40, kMinTextViewHeight)];
self.voiceMemoButton.accessibilityLabel = @"Voice message button";
self.voiceMemoButton.isAccessibilityElement = YES;
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _voiceMemoButton);
// We want to be permissive about the voice message gesture, so we hang
@ -1092,10 +1095,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,30 +1062,17 @@ 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;
};
NSMutableSet<NSString *> *interactionIds = [NSMutableSet new];
BOOL canLoadMoreItems = self.messageMapping.canLoadMore;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
NSMutableArray<TSInteraction *> *interactions = [NSMutableArray new];
@ -1280,22 +1101,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 +1339,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 +1362,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 +1390,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

Some files were not shown because too many files have changed in this diff Show More