Merge branch 'dev' into link-previews
This commit is contained in:
commit
61a0672824
|
@ -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
|
||||
-->
|
||||
|
|
|
@ -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**
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
20
BUILDING.md
20
BUILDING.md
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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
28
Podfile
|
@ -15,7 +15,7 @@ target 'Session' do
|
|||
pod 'Sodium', :inhibit_warnings => true
|
||||
pod 'SSZipArchive', :inhibit_warnings => true
|
||||
pod 'Starscream', git: 'https://github.com/signalapp/Starscream.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/signalapp/YapDatabase.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true
|
||||
pod 'YYImage', git: 'https://github.com/signalapp/YYImage', :inhibit_warnings => true
|
||||
pod 'ZXingObjC', :inhibit_warnings => true
|
||||
end
|
||||
|
@ -28,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|
|
||||
|
|
12
Podfile.lock
12
Podfile.lock
|
@ -144,7 +144,7 @@ DEPENDENCIES:
|
|||
- SSZipArchive
|
||||
- Starscream (from `https://github.com/signalapp/Starscream.git`, branch `signal-release`)
|
||||
- SwiftProtobuf (~> 1.5.0)
|
||||
- YapDatabase/SQLCipher (from `https://github.com/signalapp/YapDatabase.git`, branch `signal-release`)
|
||||
- YapDatabase/SQLCipher (from `https://github.com/loki-project/session-ios-yap-database.git`, branch `signal-release`)
|
||||
- YYImage (from `https://github.com/signalapp/YYImage`)
|
||||
- ZXingObjC
|
||||
|
||||
|
@ -181,7 +181,7 @@ EXTERNAL SOURCES:
|
|||
:git: https://github.com/signalapp/Starscream.git
|
||||
YapDatabase:
|
||||
:branch: signal-release
|
||||
:git: https://github.com/signalapp/YapDatabase.git
|
||||
:git: https://github.com/loki-project/session-ios-yap-database.git
|
||||
YYImage:
|
||||
:git: https://github.com/signalapp/YYImage
|
||||
|
||||
|
@ -199,8 +199,8 @@ CHECKOUT OPTIONS:
|
|||
:commit: b09ea163c3cb305152c65b299cb024610f52e735
|
||||
:git: https://github.com/signalapp/Starscream.git
|
||||
YapDatabase:
|
||||
:commit: e43ab163b2dcb4c817339c819b07dac545f05fea
|
||||
:git: https://github.com/signalapp/YapDatabase.git
|
||||
:commit: 5806f6b6e0b34124ee09283a9eca9ce7e6eaf14e
|
||||
:git: https://github.com/loki-project/session-ios-yap-database.git
|
||||
YYImage:
|
||||
:commit: d91910e6f313a255febbf69795198e74259bd51c
|
||||
:git: https://github.com/signalapp/YYImage
|
||||
|
@ -230,6 +230,6 @@ SPEC CHECKSUMS:
|
|||
YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: 3489ed70ea51f2bf705bf99703efc71d697de373
|
||||
PODFILE CHECKSUM: 3263ab95f60e220882ca53cca4c6bdc2e7a80381
|
||||
|
||||
COCOAPODS: 1.9.3
|
||||
COCOAPODS: 1.10.0.rc.1
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
|
||||
extension AppDelegate : OpenGroupAPIDelegate {
|
||||
|
||||
public func updateProfileIfNeeded(for channel: UInt64, on server: String, from info: OpenGroupInfo) {
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
let publicChatID = "\(server).\(channel)"
|
||||
Storage.writeSync { transaction in
|
||||
// Update user count
|
||||
storage.setUserCount(info.memberCount, forPublicChatWithID: publicChatID, in: transaction)
|
||||
let groupThread = TSGroupThread.getOrCreateThread(withGroupId: publicChatID.data(using: .utf8)!, groupType: .openGroup, transaction: transaction)
|
||||
// Update display name if needed
|
||||
let groupModel = groupThread.groupModel
|
||||
if groupModel.groupName != info.displayName {
|
||||
let newGroupModel = TSGroupModel(title: info.displayName, memberIds: groupModel.groupMemberIds, image: groupModel.groupImage, groupId: groupModel.groupId, groupType: groupModel.groupType, adminIds: groupModel.groupAdminIds)
|
||||
groupThread.groupModel = newGroupModel
|
||||
groupThread.save(with: transaction)
|
||||
}
|
||||
// Download and update profile picture if needed
|
||||
let oldProfilePictureURL = storage.getProfilePictureURL(forPublicChatWithID: publicChatID, in: transaction)
|
||||
if oldProfilePictureURL != info.profilePictureURL || groupModel.groupImage == nil {
|
||||
storage.setProfilePictureURL(info.profilePictureURL, forPublicChatWithID: publicChatID, in: transaction)
|
||||
if let profilePictureURL = info.profilePictureURL {
|
||||
var sanitizedServerURL = server
|
||||
var sanitizedProfilePictureURL = profilePictureURL
|
||||
while sanitizedServerURL.hasSuffix("/") { sanitizedServerURL.removeLast(1) }
|
||||
while sanitizedProfilePictureURL.hasPrefix("/") { sanitizedProfilePictureURL.removeFirst(1) }
|
||||
let url = "\(sanitizedServerURL)/\(sanitizedProfilePictureURL)"
|
||||
FileServerAPI.downloadAttachment(from: url).map2 { data in
|
||||
let attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(data.count), sourceFilename: nil, caption: nil, albumMessageId: nil)
|
||||
try attachmentStream.write(data)
|
||||
groupThread.updateAvatar(with: attachmentStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
|
||||
extension AppDelegate : SharedSenderKeysDelegate {
|
||||
|
||||
public func requestSenderKey(for groupPublicKey: String, senderPublicKey: String, using transaction: Any) {
|
||||
ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction as! YapDatabaseReadWriteTransaction)
|
||||
}
|
||||
}
|
|
@ -132,8 +132,7 @@ final class ConversationCell : UITableViewCell {
|
|||
// MARK: Updating
|
||||
private func update() {
|
||||
AssertIsOnMainThread()
|
||||
let thread = threadViewModel.threadRecord
|
||||
guard let threadID = thread.uniqueId else { return }
|
||||
guard let thread = threadViewModel?.threadRecord, let threadID = thread.uniqueId else { return }
|
||||
MentionsManager.populateUserPublicKeyCacheIfNeeded(for: threadID) // FIXME: This is a terrible place to do this
|
||||
let isBlocked: Bool
|
||||
if let thread = thread as? TSContactThread {
|
||||
|
@ -166,7 +165,7 @@ final class ConversationCell : UITableViewCell {
|
|||
let image: UIImage
|
||||
let status = MessageRecipientStatusUtils.recipientStatus(outgoingMessage: lastMessage)
|
||||
switch status {
|
||||
case .calculatingPoW, .uploading, .sending: image = #imageLiteral(resourceName: "CircleDotDotDot").asTintedImage(color: Colors.text)!
|
||||
case .uploading, .sending: image = #imageLiteral(resourceName: "CircleDotDotDot").asTintedImage(color: Colors.text)!
|
||||
case .sent, .skipped, .delivered: image = #imageLiteral(resourceName: "CircleCheck").asTintedImage(color: Colors.text)!
|
||||
case .read:
|
||||
statusIndicatorView.backgroundColor = isLightMode ? .black : .white
|
||||
|
|
|
@ -50,11 +50,11 @@ final class ConversationTitleView : UIView {
|
|||
updateSubtitleForCurrentStatus()
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(handleProfileChangedNotification(_:)), name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange), object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleCalculatingPoWNotification(_:)), name: .calculatingPoW, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleRoutingNotification(_:)), name: .routing, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleCalculatingMessagePoWNotification(_:)), name: .calculatingMessagePoW, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleEncryptingMessageNotification(_:)), name: .encryptingMessage, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageSendingNotification(_:)), name: .messageSending, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageSentNotification(_:)), name: .messageSent, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageFailedNotification(_:)), name: .messageFailed, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageSendingFailedNotification(_:)), name: .messageSendingFailed, object: nil)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
@ -112,12 +112,12 @@ final class ConversationTitleView : UIView {
|
|||
updateProfilePicture()
|
||||
}
|
||||
|
||||
@objc private func handleCalculatingPoWNotification(_ notification: Notification) {
|
||||
@objc private func handleCalculatingMessagePoWNotification(_ notification: Notification) {
|
||||
guard let timestamp = notification.object as? NSNumber else { return }
|
||||
setStatusIfNeeded(to: .calculatingPoW, forMessageWithTimestamp: timestamp)
|
||||
}
|
||||
|
||||
@objc private func handleRoutingNotification(_ notification: Notification) {
|
||||
@objc private func handleEncryptingMessageNotification(_ notification: Notification) {
|
||||
guard let timestamp = notification.object as? NSNumber else { return }
|
||||
setStatusIfNeeded(to: .routing, forMessageWithTimestamp: timestamp)
|
||||
}
|
||||
|
@ -136,7 +136,7 @@ final class ConversationTitleView : UIView {
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func handleMessageFailedNotification(_ notification: Notification) {
|
||||
@objc private func handleMessageSendingFailedNotification(_ notification: Notification) {
|
||||
guard let timestamp = notification.object as? NSNumber else { return }
|
||||
clearStatusIfNeededForMessageWithTimestamp(timestamp)
|
||||
}
|
||||
|
@ -149,14 +149,7 @@ final class ConversationTitleView : UIView {
|
|||
uncheckedTargetInteraction = interaction
|
||||
}
|
||||
guard let targetInteraction = uncheckedTargetInteraction, targetInteraction.interactionType() == .outgoingMessage,
|
||||
status.rawValue > (currentStatus?.rawValue ?? 0), let hexEncodedPublicKey = targetInteraction.thread.contactIdentifier() else { return }
|
||||
var masterHexEncodedPublicKey: String!
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
storage.dbReadConnection.read { transaction in
|
||||
masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) ?? hexEncodedPublicKey
|
||||
}
|
||||
let isSlaveDevice = masterHexEncodedPublicKey != hexEncodedPublicKey
|
||||
guard !isSlaveDevice else { return }
|
||||
status.rawValue > (currentStatus?.rawValue ?? 0) else { return }
|
||||
currentStatus = status
|
||||
}
|
||||
|
||||
|
@ -174,7 +167,8 @@ final class ConversationTitleView : UIView {
|
|||
}
|
||||
|
||||
@objc func updateSubtitleForCurrentStatus() {
|
||||
DispatchQueue.main.async {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.subtitleLabel.isHidden = false
|
||||
let subtitle = NSMutableAttributedString()
|
||||
if let muteEndDate = self.thread.mutedUntilDate, self.thread.isMuted {
|
||||
|
@ -184,16 +178,13 @@ final class ConversationTitleView : UIView {
|
|||
dateFormatter.timeStyle = .medium
|
||||
dateFormatter.dateStyle = .medium
|
||||
subtitle.append(NSAttributedString(string: "Muted until " + dateFormatter.string(from: muteEndDate)))
|
||||
} else if let thread = self.thread as? TSGroupThread, !thread.isRSSFeed {
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
} else if let thread = self.thread as? TSGroupThread {
|
||||
var userCount: Int?
|
||||
if thread.groupModel.groupType == .closedGroup {
|
||||
userCount = GroupUtilities.getClosedGroupMemberCount(thread)
|
||||
} else if thread.groupModel.groupType == .openGroup {
|
||||
storage.dbReadConnection.read { transaction in
|
||||
if let publicChat = LokiDatabaseUtilities.getPublicChat(for: self.thread.uniqueId!, in: transaction) {
|
||||
userCount = storage.getUserCount(for: publicChat, in: transaction)
|
||||
}
|
||||
if let openGroup = Storage.shared.getOpenGroup(for: self.thread.uniqueId!) {
|
||||
userCount = Storage.shared.getUserCount(forOpenGroupWithID: openGroup.id)
|
||||
}
|
||||
}
|
||||
if let userCount = userCount {
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
|
||||
// MARK: - User Selection View
|
||||
|
||||
@objc(LKMentionCandidateSelectionView)
|
||||
final class MentionCandidateSelectionView : UIView, UITableViewDataSource, UITableViewDelegate {
|
||||
@objc var mentionCandidates: [Mention] = [] { didSet { tableView.reloadData() } }
|
||||
|
@ -173,7 +171,8 @@ private extension MentionCandidateSelectionView {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: Delegate
|
||||
// MARK: - Delegate
|
||||
|
||||
@objc(LKMentionCandidateSelectionViewDelegate)
|
||||
protocol MentionCandidateSelectionViewDelegate {
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -34,7 +34,7 @@ final class OptionView : UIView {
|
|||
// Set up shadow
|
||||
layer.shadowColor = UIColor.black.cgColor
|
||||
layer.shadowOffset = CGSize(width: 0, height: 0.8)
|
||||
layer.shadowOpacity = isLightMode ? 0.4 : 1
|
||||
layer.shadowOpacity = isLightMode ? 0.16 : 1
|
||||
layer.shadowRadius = isLightMode ? 4 : 6
|
||||
// Set up title label
|
||||
let titleLabel = UILabel()
|
||||
|
|
|
@ -18,7 +18,7 @@ final class PathStatusView : UIView {
|
|||
layer.cornerRadius = Values.pathStatusViewSize / 2
|
||||
layer.masksToBounds = false
|
||||
if OnionRequestAPI.paths.isEmpty {
|
||||
OnionRequestAPI.paths = Storage.getOnionRequestPaths()
|
||||
OnionRequestAPI.paths = Storage.shared.getOnionRequestPaths()
|
||||
}
|
||||
let color = (!OnionRequestAPI.paths.isEmpty) ? Colors.accent : Colors.pathsBuilding
|
||||
setColor(to: color, isAnimated: false)
|
||||
|
|
|
@ -65,9 +65,9 @@ final class VoiceMessageView : UIView {
|
|||
setUpViewHierarchy()
|
||||
if voiceMessage.isDownloaded {
|
||||
guard let url = (voiceMessage as? TSAttachmentStream)?.originalMediaURL else {
|
||||
return print("[Loki] Couldn't get URL for voice message.")
|
||||
return SNLog("Couldn't get URL for voice message.")
|
||||
}
|
||||
if let cachedVolumeSamples = Storage.getVolumeSamples(for: voiceMessage.uniqueId!), cachedVolumeSamples.count == targetSampleCount {
|
||||
if let cachedVolumeSamples = Storage.shared.getVolumeSamples(for: voiceMessage.uniqueId!), cachedVolumeSamples.count == targetSampleCount {
|
||||
self.hideLoader()
|
||||
self.volumeSamples = cachedVolumeSamples
|
||||
} else {
|
||||
|
@ -78,10 +78,10 @@ final class VoiceMessageView : UIView {
|
|||
self.isForcedAnimation = true
|
||||
self.volumeSamples = volumeSamples
|
||||
Storage.write { transaction in
|
||||
Storage.setVolumeSamples(for: voiceMessageID, to: volumeSamples, using: transaction)
|
||||
Storage.shared.setVolumeSamples(for: voiceMessageID, to: volumeSamples, using: transaction)
|
||||
}
|
||||
}.catch(on: DispatchQueue.main) { error in
|
||||
print("[Loki] Couldn't sample audio file due to error: \(error).")
|
||||
SNLog("Couldn't sample audio file due to error: \(error).")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
|
||||
extension Storage : SessionProtocolKitStorageProtocol {
|
||||
|
||||
private func getClosedGroupRatchetCollection(_ collection: ClosedGroupRatchetCollectionType, for groupPublicKey: String) -> String {
|
||||
switch collection {
|
||||
case .old: return "LokiOldClosedGroupRatchetCollection.\(groupPublicKey)"
|
||||
case .current: return "LokiClosedGroupRatchetCollection.\(groupPublicKey)"
|
||||
}
|
||||
}
|
||||
|
||||
public func getClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, from collection: ClosedGroupRatchetCollectionType = .current) -> ClosedGroupRatchet? {
|
||||
let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey)
|
||||
var result: ClosedGroupRatchet?
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: senderPublicKey, inCollection: collection) as? ClosedGroupRatchet
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setClosedGroupRatchet(for groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupRatchet, in collection: ClosedGroupRatchetCollectionType = .current, using transaction: Any) {
|
||||
let collection = getClosedGroupRatchetCollection(collection, for: groupPublicKey)
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(ratchet, forKey: senderPublicKey, inCollection: collection)
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
extension Storage {
|
||||
|
||||
public static let shared = Storage()
|
||||
|
||||
public func with(_ work: @escaping (Any) -> Void) {
|
||||
Storage.writeSync { work($0) }
|
||||
}
|
||||
|
||||
public func withAsync(_ work: @escaping (Any) -> Void, completion: @escaping () -> Void) {
|
||||
Storage.write(with: { work($0) }, completion: completion)
|
||||
}
|
||||
|
||||
public func getUserPublicKey() -> String? {
|
||||
return OWSIdentityManager.shared().identityKeyPair()?.publicKey.toHexString()
|
||||
}
|
||||
|
||||
public func getUserKeyPair() -> ECKeyPair? {
|
||||
return OWSIdentityManager.shared().identityKeyPair()
|
||||
}
|
||||
|
||||
public func getUserDisplayName() -> String? { fatalError() }
|
||||
}
|
|
@ -1,17 +1,17 @@
|
|||
|
||||
extension Storage {
|
||||
|
||||
static let volumeSamplesCollection = "LokiVolumeSamplesCollection"
|
||||
private static let volumeSamplesCollection = "LokiVolumeSamplesCollection"
|
||||
|
||||
static func getVolumeSamples(for attachment: String) -> [Float]? {
|
||||
public func getVolumeSamples(for attachment: String) -> [Float]? {
|
||||
var result: [Float]?
|
||||
read { transaction in
|
||||
result = transaction.object(forKey: attachment, inCollection: volumeSamplesCollection) as? [Float]
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: attachment, inCollection: Storage.volumeSamplesCollection) as? [Float]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
static func setVolumeSamples(for attachment: String, to volumeSamples: [Float], using transaction: YapDatabaseReadWriteTransaction) {
|
||||
transaction.setObject(volumeSamples, forKey: attachment, inCollection: volumeSamplesCollection)
|
||||
public func setVolumeSamples(for attachment: String, to volumeSamples: [Float], using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(volumeSamples, forKey: attachment, inCollection: Storage.volumeSamplesCollection)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Shield.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -16,10 +16,8 @@
|
|||
#import "ConversationViewCell.h"
|
||||
#import "ConversationViewItem.h"
|
||||
#import "DateUtil.h"
|
||||
#import "FingerprintViewController.h"
|
||||
#import "MediaDetailViewController.h"
|
||||
#import "NotificationSettingsViewController.h"
|
||||
#import "OWSAddToContactViewController.h"
|
||||
#import "OWSAnyTouchGestureRecognizer.h"
|
||||
#import "OWSAudioPlayer.h"
|
||||
#import "OWSBackup.h"
|
||||
|
@ -35,13 +33,11 @@
|
|||
#import "OWSQuotedMessageView.h"
|
||||
#import "OWSSessionResetJobRecord.h"
|
||||
#import "OWSWindowManager.h"
|
||||
#import "PinEntryView.h"
|
||||
#import "PrivacySettingsTableViewController.h"
|
||||
#import "RemoteVideoView.h"
|
||||
#import "OWSQRCodeScanningViewController.h"
|
||||
#import "SignalApp.h"
|
||||
#import "UIViewController+Permissions.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
#import <SessionProtocolKit/NSData+keyVersionByte.h>
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import <Reachability/Reachability.h>
|
||||
|
@ -53,73 +49,50 @@
|
|||
#import <SignalCoreKit/Threading.h>
|
||||
#import <SignalUtilitiesKit/AttachmentSharing.h>
|
||||
#import <SignalUtilitiesKit/ContactTableViewCell.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSAudioPlayer.h>
|
||||
#import <SignalUtilitiesKit/OWSContactAvatarBuilder.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SessionMessagingKit/OWSAudioPlayer.h>
|
||||
#import <SignalUtilitiesKit/OWSFormat.h>
|
||||
#import <SignalUtilitiesKit/OWSPreferences.h>
|
||||
#import <SessionMessagingKit/OWSPreferences.h>
|
||||
#import <SignalUtilitiesKit/OWSProfileManager.h>
|
||||
#import <SignalUtilitiesKit/OWSQuotedReplyModel.h>
|
||||
#import <SignalUtilitiesKit/OWSSounds.h>
|
||||
#import <SessionMessagingKit/OWSQuotedReplyModel.h>
|
||||
#import <SessionMessagingKit/OWSSounds.h>
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
#import <SignalUtilitiesKit/ThreadUtil.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIFont+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIViewController+OWS.h>
|
||||
#import <SignalUtilitiesKit/AppVersion.h>
|
||||
#import <SignalUtilitiesKit/Contact.h>
|
||||
#import <SignalUtilitiesKit/ContactsUpdater.h>
|
||||
#import <SignalUtilitiesKit/DataSource.h>
|
||||
#import <SignalUtilitiesKit/MIMETypeUtil.h>
|
||||
#import <SignalUtilitiesKit/NSData+Image.h>
|
||||
#import <SignalUtilitiesKit/NSNotificationCenter+OWS.h>
|
||||
#import <SignalUtilitiesKit/NSString+SSK.h>
|
||||
#import <SignalUtilitiesKit/NSTimer+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSAnalytics.h>
|
||||
#import <SignalUtilitiesKit/OWSAnalyticsEvents.h>
|
||||
#import <SignalUtilitiesKit/OWSBackgroundTask.h>
|
||||
#import <SignalUtilitiesKit/OWSCallMessageHandler.h>
|
||||
#import <SessionUtilitiesKit/DataSource.h>
|
||||
#import <SessionUtilitiesKit/MIMETypeUtil.h>
|
||||
#import <SessionUtilitiesKit/NSData+Image.h>
|
||||
#import <SessionUtilitiesKit/NSNotificationCenter+OWS.h>
|
||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||
#import <SessionMessagingKit/OWSBackgroundTask.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsOutputStream.h>
|
||||
#import <SignalUtilitiesKit/OWSDispatch.h>
|
||||
#import <SignalUtilitiesKit/OWSEndSessionMessage.h>
|
||||
#import <SignalUtilitiesKit/LKDeviceLinkMessage.h>
|
||||
#import <SignalUtilitiesKit/OWSError.h>
|
||||
#import <SignalUtilitiesKit/OWSFileSystem.h>
|
||||
#import <SignalUtilitiesKit/OWSIdentityManager.h>
|
||||
#import <SignalUtilitiesKit/OWSMediaGalleryFinder.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageManager.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageReceiver.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageSender.h>
|
||||
#import <SignalUtilitiesKit/OWSOutgoingCallMessage.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+Calling.h>
|
||||
#import <SessionUtilitiesKit/OWSFileSystem.h>
|
||||
#import <SessionMessagingKit/OWSIdentityManager.h>
|
||||
#import <SessionMessagingKit/OWSMediaGalleryFinder.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+SessionStore.h>
|
||||
#import <SignalUtilitiesKit/OWSProfileKeyMessage.h>
|
||||
#import <SignalUtilitiesKit/OWSRecipientIdentity.h>
|
||||
#import <SignalUtilitiesKit/OWSRequestFactory.h>
|
||||
#import <SignalUtilitiesKit/OWSSignalService.h>
|
||||
#import <SignalUtilitiesKit/PhoneNumber.h>
|
||||
#import <SessionMessagingKit/OWSRecipientIdentity.h>
|
||||
#import <SignalUtilitiesKit/SignalAccount.h>
|
||||
#import <SignalUtilitiesKit/SignalRecipient.h>
|
||||
#import <SignalUtilitiesKit/TSAccountManager.h>
|
||||
#import <SignalUtilitiesKit/TSAttachment.h>
|
||||
#import <SignalUtilitiesKit/TSAttachmentPointer.h>
|
||||
#import <SignalUtilitiesKit/TSAttachmentStream.h>
|
||||
#import <SignalUtilitiesKit/TSCall.h>
|
||||
#import <SignalUtilitiesKit/TSContactThread.h>
|
||||
#import <SignalUtilitiesKit/TSErrorMessage.h>
|
||||
#import <SignalUtilitiesKit/TSGroupThread.h>
|
||||
#import <SignalUtilitiesKit/TSIncomingMessage.h>
|
||||
#import <SignalUtilitiesKit/TSInfoMessage.h>
|
||||
#import <SignalUtilitiesKit/TSNetworkManager.h>
|
||||
#import <SignalUtilitiesKit/TSOutgoingMessage.h>
|
||||
#import <SessionMessagingKit/SignalRecipient.h>
|
||||
#import <SessionMessagingKit/TSAccountManager.h>
|
||||
#import <SessionMessagingKit/TSAttachment.h>
|
||||
#import <SessionMessagingKit/TSAttachmentPointer.h>
|
||||
#import <SessionMessagingKit/TSAttachmentStream.h>
|
||||
#import <SessionMessagingKit/TSContactThread.h>
|
||||
#import <SessionMessagingKit/TSErrorMessage.h>
|
||||
#import <SessionMessagingKit/TSGroupThread.h>
|
||||
#import <SessionMessagingKit/TSIncomingMessage.h>
|
||||
#import <SessionMessagingKit/TSInfoMessage.h>
|
||||
#import <SessionMessagingKit/TSOutgoingMessage.h>
|
||||
#import <SignalUtilitiesKit/TSPreKeyManager.h>
|
||||
#import <SignalUtilitiesKit/TSSocketManager.h>
|
||||
#import <SignalUtilitiesKit/TSThread.h>
|
||||
#import <SignalUtilitiesKit/LKGroupUtilities.h>
|
||||
#import <SignalUtilitiesKit/UIImage+OWS.h>
|
||||
#import <SessionMessagingKit/TSThread.h>
|
||||
#import <SessionUtilitiesKit/LKGroupUtilities.h>
|
||||
#import <SessionUtilitiesKit/UIImage+OWS.h>
|
||||
#import <WebRTC/RTCAudioSession.h>
|
||||
#import <WebRTC/RTCCameraPreviewView.h>
|
||||
#import <YYImage/YYImage.h>
|
||||
|
|
|
@ -18,8 +18,7 @@
|
|||
#import <SignalCoreKit/NSObject+OWS.h>
|
||||
#import <SignalCoreKit/OWSAsserts.h>
|
||||
#import <SignalUtilitiesKit/SSKAsserts.h>
|
||||
#import <SignalUtilitiesKit/OWSAnalytics.h>
|
||||
#import <SignalUtilitiesKit/NSArray+Functional.h>
|
||||
#import <SessionUtilitiesKit/NSArray+Functional.h>
|
||||
#import <SignalUtilitiesKit/NSSet+Functional.h>
|
||||
#import <SignalUtilitiesKit/NSObject+Casting.h>
|
||||
#import <SessionUIKit/SessionUIKit.h>
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
#import "AboutTableViewController.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSPreferences.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SessionMessagingKit/OWSPreferences.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage.h>
|
||||
#import <SignalUtilitiesKit/TSDatabaseView.h>
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
#import <SessionMessagingKit/TSDatabaseView.h>
|
||||
|
||||
@implementation AboutTableViewController
|
||||
|
||||
|
|
|
@ -19,10 +19,6 @@ public class AccountManager: NSObject {
|
|||
return OWSProfileManager.shared()
|
||||
}
|
||||
|
||||
private var networkManager: TSNetworkManager {
|
||||
return SSKEnvironment.shared.networkManager
|
||||
}
|
||||
|
||||
private var preferences: OWSPreferences {
|
||||
return Environment.shared.preferences
|
||||
}
|
||||
|
@ -119,28 +115,4 @@ public class AccountManager: NSObject {
|
|||
let anyPromise = tsAccountManager.setIsManualMessageFetchEnabled(true)
|
||||
return Promise(anyPromise).asVoid()
|
||||
}
|
||||
|
||||
// MARK: Turn Server
|
||||
|
||||
func getTurnServerInfo() -> Promise<TurnServerInfo> {
|
||||
return Promise { resolver in
|
||||
self.networkManager.makeRequest(OWSRequestFactory.turnServerInfoRequest(),
|
||||
success: { (_: URLSessionDataTask, responseObject: Any?) in
|
||||
guard responseObject != nil else {
|
||||
return resolver.reject(OWSErrorMakeUnableToProcessServerResponseError())
|
||||
}
|
||||
|
||||
if let responseDictionary = responseObject as? [String: AnyObject] {
|
||||
if let turnServerInfo = TurnServerInfo(attributes: responseDictionary) {
|
||||
return resolver.fulfill(turnServerInfo)
|
||||
}
|
||||
Logger.error("unexpected server response:\(responseDictionary)")
|
||||
}
|
||||
return resolver.reject(OWSErrorMakeUnableToProcessServerResponseError())
|
||||
},
|
||||
failure: { (_: URLSessionDataTask, error: Error) in
|
||||
return resolver.reject(error)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,145 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import ContactsUI
|
||||
|
||||
class AddContactShareToExistingContactViewController: ContactsPicker, ContactsPickerDelegate, CNContactViewControllerDelegate {
|
||||
|
||||
// TODO - there are some hard coded assumptions in this VC that assume we are *pushed* onto a
|
||||
// navigation controller. That seems fine for now, but if we need to be presented as a modal,
|
||||
// or need to notify our presenter about our dismisall or other contact actions, a delegate
|
||||
// would be helpful. It seems like this would require some broad changes to the ContactShareViewHelper,
|
||||
// so I've left it as is for now, since it happens to work.
|
||||
// weak var addToExistingContactDelegate: AddContactShareToExistingContactViewControllerDelegate?
|
||||
|
||||
let contactShare: ContactShareViewModel
|
||||
|
||||
required init(contactShare: ContactShareViewModel) {
|
||||
self.contactShare = contactShare
|
||||
super.init(allowsMultipleSelection: false, subtitleCellType: .none)
|
||||
|
||||
self.contactsPickerDelegate = self
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
@objc required public init(allowsMultipleSelection: Bool, subtitleCellType: SubtitleCellValue) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
// MARK: - ContactsPickerDelegate
|
||||
|
||||
func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError) {
|
||||
owsFailDebug("with error: \(error)")
|
||||
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
navigationController.popViewController(animated: true)
|
||||
}
|
||||
|
||||
func contactsPickerDidCancel(_: ContactsPicker) {
|
||||
Logger.debug("")
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
navigationController.popViewController(animated: true)
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, didSelectContact oldContact: Contact) {
|
||||
Logger.debug("")
|
||||
|
||||
let contactsManager = Environment.shared.contactsManager
|
||||
guard let oldCNContact = contactsManager?.cnContact(withId: oldContact.cnContactId) else {
|
||||
owsFailDebug("could not load old CNContact.")
|
||||
return
|
||||
}
|
||||
guard let newCNContact = OWSContacts.systemContact(for: self.contactShare.dbRecord, imageData: self.contactShare.avatarImageData) else {
|
||||
owsFailDebug("could not load new CNContact.")
|
||||
return
|
||||
}
|
||||
merge(oldCNContact: oldCNContact, newCNContact: newCNContact)
|
||||
}
|
||||
|
||||
func merge(oldCNContact: CNContact, newCNContact: CNContact) {
|
||||
Logger.debug("")
|
||||
|
||||
let mergedCNContact: CNContact = Contact.merge(cnContact: oldCNContact, newCNContact: newCNContact)
|
||||
|
||||
// Not actually a "new" contact, but this brings up the edit form rather than the "Read" form
|
||||
// saving our users a tap in some cases when we already know they want to edit.
|
||||
let contactViewController: CNContactViewController = CNContactViewController(forNewContact: mergedCNContact)
|
||||
|
||||
// Default title is "New Contact". We could give a more descriptive title, but anything
|
||||
// seems redundant - the context is sufficiently clear.
|
||||
contactViewController.title = ""
|
||||
contactViewController.allowsActions = false
|
||||
contactViewController.allowsEditing = true
|
||||
contactViewController.delegate = self
|
||||
|
||||
let modal = OWSNavigationController(rootViewController: contactViewController)
|
||||
self.present(modal, animated: true)
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact]) {
|
||||
Logger.debug("")
|
||||
owsFailDebug("only supports single contact select")
|
||||
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
navigationController.popViewController(animated: true)
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - CNContactViewControllerDelegate
|
||||
|
||||
public func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
|
||||
Logger.debug("")
|
||||
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
// TODO this is weird - ideally we'd do something like
|
||||
// self.delegate?.didFinishAddingContact
|
||||
// and the delegate, which knows about our presentation context could do the right thing.
|
||||
//
|
||||
// As it is, we happen to always be *pushing* this view controller onto a navcontroller, so the
|
||||
// following works in all current cases.
|
||||
//
|
||||
// If we ever wanted to do something different, like present this in a modal, we'd have to rethink.
|
||||
|
||||
// We want to pop *this* view *and* the still presented CNContactViewController in a single animation.
|
||||
// Note this happens for *cancel* and for *done*. Unfortunately, I don't know of a way to detect the difference
|
||||
// between the two, since both just call this method.
|
||||
guard let myIndex = navigationController.viewControllers.firstIndex(of: self) else {
|
||||
owsFailDebug("myIndex was unexpectedly nil")
|
||||
navigationController.popViewController(animated: true)
|
||||
navigationController.popViewController(animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
let previousViewControllerIndex = navigationController.viewControllers.index(before: myIndex)
|
||||
let previousViewController = navigationController.viewControllers[previousViewControllerIndex]
|
||||
|
||||
self.dismiss(animated: false) {
|
||||
navigationController.popToViewController(previousViewController, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
#import "AddToBlockListViewController.h"
|
||||
#import "BlockListUIUtils.h"
|
||||
#import "ContactsViewHelper.h"
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SessionMessagingKit/SSKEnvironment.h>
|
||||
#import <SessionMessagingKit/OWSBlockingManager.h>
|
||||
#import <SignalUtilitiesKit/SignalAccount.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
@ -51,8 +51,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
__weak AddToBlockListViewController *weakSelf = self;
|
||||
[BlockListUIUtils showBlockPhoneNumberActionSheet:phoneNumber
|
||||
fromViewController:self
|
||||
blockingManager:self.contactsViewHelper.blockingManager
|
||||
contactsManager:self.contactsViewHelper.contactsManager
|
||||
blockingManager:SSKEnvironment.shared.blockingManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
if (isBlocked) {
|
||||
[weakSelf.navigationController popViewControllerAnimated:YES];
|
||||
|
@ -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];
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SelectRecipientViewController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol AddToGroupViewControllerDelegate <NSObject>
|
||||
|
||||
- (void)recipientIdWasAdded:(NSString *)recipientId;
|
||||
|
||||
- (BOOL)isRecipientGroupMember:(NSString *)recipientId;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface AddToGroupViewController : SelectRecipientViewController
|
||||
|
||||
@property (nonatomic, weak) id<AddToGroupViewControllerDelegate> addToGroupDelegate;
|
||||
|
||||
@property (nonatomic) BOOL hideContacts;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,174 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AddToGroupViewController.h"
|
||||
#import "BlockListUIUtils.h"
|
||||
#import "ContactsViewHelper.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/SignalAccount.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AddToGroupViewController () <SelectRecipientViewControllerDelegate>
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation AddToGroupViewController
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
self.delegate = self;
|
||||
|
||||
[super loadView];
|
||||
|
||||
self.title = NSLocalizedString(@"ADD_GROUP_MEMBER_VIEW_TITLE", @"Title for the 'add group member' view.");
|
||||
}
|
||||
|
||||
- (NSString *)phoneNumberSectionTitle
|
||||
{
|
||||
return NSLocalizedString(@"ADD_GROUP_MEMBER_VIEW_PHONE_NUMBER_TITLE",
|
||||
@"Title for the 'add by phone number' section of the 'add group member' view.");
|
||||
}
|
||||
|
||||
- (NSString *)phoneNumberButtonText
|
||||
{
|
||||
return NSLocalizedString(@"ADD_GROUP_MEMBER_VIEW_BUTTON",
|
||||
@"A label for the 'add by phone number' button in the 'add group member' view");
|
||||
}
|
||||
|
||||
- (NSString *)contactsSectionTitle
|
||||
{
|
||||
return NSLocalizedString(
|
||||
@"ADD_GROUP_MEMBER_VIEW_CONTACT_TITLE", @"Title for the 'add contact' section of the 'add group member' view.");
|
||||
}
|
||||
|
||||
- (void)phoneNumberWasSelected:(NSString *)phoneNumber
|
||||
{
|
||||
OWSAssertDebug(phoneNumber.length > 0);
|
||||
|
||||
__weak AddToGroupViewController *weakSelf = self;
|
||||
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
if ([helper isRecipientIdBlocked:phoneNumber]) {
|
||||
[BlockListUIUtils showUnblockPhoneNumberActionSheet:phoneNumber
|
||||
fromViewController:self
|
||||
blockingManager:helper.blockingManager
|
||||
contactsManager:helper.contactsManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
if (!isBlocked) {
|
||||
[weakSelf addToGroup:phoneNumber];
|
||||
}
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL didShowSNAlert = [SafetyNumberConfirmationAlert
|
||||
presentAlertIfNecessaryWithRecipientId:phoneNumber
|
||||
confirmationText:
|
||||
NSLocalizedString(@"SAFETY_NUMBER_CHANGED_CONFIRM_ADD_TO_GROUP_ACTION",
|
||||
@"button title to confirm adding a recipient to a group when their safety "
|
||||
@"number has recently changed")
|
||||
contactsManager:helper.contactsManager
|
||||
completion:^(BOOL didConfirmIdentity) {
|
||||
if (didConfirmIdentity) {
|
||||
[weakSelf addToGroup:phoneNumber];
|
||||
}
|
||||
}];
|
||||
if (didShowSNAlert) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self addToGroup:phoneNumber];
|
||||
}
|
||||
|
||||
- (BOOL)canSignalAccountBeSelected:(SignalAccount *)signalAccount
|
||||
{
|
||||
OWSAssertDebug(signalAccount);
|
||||
|
||||
return ![self.addToGroupDelegate isRecipientGroupMember:signalAccount.recipientId];
|
||||
}
|
||||
|
||||
- (void)signalAccountWasSelected:(SignalAccount *)signalAccount
|
||||
{
|
||||
OWSAssertDebug(signalAccount);
|
||||
|
||||
__weak AddToGroupViewController *weakSelf = self;
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
if ([self.addToGroupDelegate isRecipientGroupMember:signalAccount.recipientId]) {
|
||||
OWSFailDebug(@"Cannot add user to group member if already a member.");
|
||||
return;
|
||||
}
|
||||
|
||||
if ([helper isRecipientIdBlocked:signalAccount.recipientId]) {
|
||||
[BlockListUIUtils showUnblockSignalAccountActionSheet:signalAccount
|
||||
fromViewController:self
|
||||
blockingManager:helper.blockingManager
|
||||
contactsManager:helper.contactsManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
if (!isBlocked) {
|
||||
[weakSelf addToGroup:signalAccount.recipientId];
|
||||
}
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL didShowSNAlert = [SafetyNumberConfirmationAlert
|
||||
presentAlertIfNecessaryWithRecipientId:signalAccount.recipientId
|
||||
confirmationText:
|
||||
NSLocalizedString(@"SAFETY_NUMBER_CHANGED_CONFIRM_ADD_TO_GROUP_ACTION",
|
||||
@"button title to confirm adding a recipient to a group when their safety "
|
||||
@"number has recently changed")
|
||||
contactsManager:helper.contactsManager
|
||||
completion:^(BOOL didConfirmIdentity) {
|
||||
if (didConfirmIdentity) {
|
||||
[weakSelf addToGroup:signalAccount.recipientId];
|
||||
}
|
||||
}];
|
||||
if (didShowSNAlert) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self addToGroup:signalAccount.recipientId];
|
||||
}
|
||||
|
||||
- (void)addToGroup:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
[self.addToGroupDelegate recipientIdWasAdded:recipientId];
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
- (BOOL)shouldHideLocalNumber
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)shouldHideContacts
|
||||
{
|
||||
return self.hideContacts;
|
||||
}
|
||||
|
||||
- (BOOL)shouldValidatePhoneNumbers
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (nullable NSString *)accessoryMessageForSignalAccount:(SignalAccount *)signalAccount
|
||||
{
|
||||
OWSAssertDebug(signalAccount);
|
||||
|
||||
if ([self.addToGroupDelegate isRecipientGroupMember:signalAccount.recipientId]) {
|
||||
return NSLocalizedString(@"NEW_GROUP_MEMBER_LABEL", @"An indicator that a user is a member of the new group.");
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,9 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSTableViewController.h"
|
||||
|
||||
@interface AdvancedSettingsTableViewController : OWSTableViewController
|
||||
|
||||
@end
|
|
@ -1,312 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AdvancedSettingsTableViewController.h"
|
||||
#import "DebugLogger.h"
|
||||
#import "DomainFrontingCountryViewController.h"
|
||||
#import "OWSCountryMetadata.h"
|
||||
#import "Pastelog.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "TSAccountManager.h"
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <Reachability/Reachability.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSPreferences.h>
|
||||
#import <SignalUtilitiesKit/OWSSignalService.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AdvancedSettingsTableViewController ()
|
||||
|
||||
@property (nonatomic) Reachability *reachability;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation AdvancedSettingsTableViewController
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
self.title = NSLocalizedString(@"SETTINGS_ADVANCED_TITLE", @"");
|
||||
|
||||
self.reachability = [Reachability reachabilityForInternetConnection];
|
||||
|
||||
[self observeNotifications];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)observeNotifications
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(socketStateDidChange)
|
||||
name:kNSNotification_OWSWebSocketStateDidChange
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(reachabilityChanged)
|
||||
name:kReachabilityChangedNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)socketStateDidChange
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)reachabilityChanged
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - Table Contents
|
||||
|
||||
- (void)updateTableContents
|
||||
{
|
||||
OWSTableContents *contents = [OWSTableContents new];
|
||||
|
||||
__weak AdvancedSettingsTableViewController *weakSelf = self;
|
||||
|
||||
OWSTableSection *loggingSection = [OWSTableSection new];
|
||||
loggingSection.headerTitle = NSLocalizedString(@"LOGGING_SECTION", nil);
|
||||
[loggingSection addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"SETTINGS_ADVANCED_DEBUGLOG", @"")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"enable_debug_log")
|
||||
isOnBlock:^{
|
||||
return [OWSPreferences isLoggingEnabled];
|
||||
}
|
||||
isEnabledBlock:^{
|
||||
return YES;
|
||||
}
|
||||
target:weakSelf
|
||||
selector:@selector(didToggleEnableLogSwitch:)]];
|
||||
|
||||
|
||||
if ([OWSPreferences isLoggingEnabled]) {
|
||||
[loggingSection
|
||||
addItem:[OWSTableItem actionItemWithText:NSLocalizedString(@"SETTINGS_ADVANCED_SUBMIT_DEBUGLOG", @"")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"submit_debug_log")
|
||||
actionBlock:^{
|
||||
OWSLogInfo(@"Submitting debug logs");
|
||||
[DDLog flushLog];
|
||||
[Pastelog submitLogs];
|
||||
}]];
|
||||
}
|
||||
|
||||
[contents addSection:loggingSection];
|
||||
|
||||
OWSTableSection *pushNotificationsSection = [OWSTableSection new];
|
||||
pushNotificationsSection.headerTitle
|
||||
= NSLocalizedString(@"PUSH_REGISTER_TITLE", @"Used in table section header and alert view title contexts");
|
||||
[pushNotificationsSection addItem:[OWSTableItem actionItemWithText:NSLocalizedString(@"REREGISTER_FOR_PUSH", nil)
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(
|
||||
self, @"reregister_push_notifications")
|
||||
actionBlock:^{
|
||||
[weakSelf syncPushTokens];
|
||||
}]];
|
||||
[contents addSection:pushNotificationsSection];
|
||||
|
||||
// Censorship circumvention has certain disadvantages so it should only be
|
||||
// used if necessary. Therefore:
|
||||
//
|
||||
// * We disable this setting if the user has a phone number from a censored region -
|
||||
// censorship circumvention will be auto-activated for this user.
|
||||
// * We disable this setting if the user is already connected; they're not being
|
||||
// censored.
|
||||
// * We continue to show this setting so long as it is set to allow users to disable
|
||||
// it, for example when they leave a censored region.
|
||||
OWSTableSection *censorshipSection = [OWSTableSection new];
|
||||
censorshipSection.headerTitle = NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_HEADER",
|
||||
@"Table header for the 'censorship circumvention' section.");
|
||||
BOOL isAnySocketOpen = TSSocketManager.shared.highestSocketState == OWSWebSocketStateOpen;
|
||||
if (OWSSignalService.sharedInstance.hasCensoredPhoneNumber) {
|
||||
if (OWSSignalService.sharedInstance.isCensorshipCircumventionManuallyDisabled) {
|
||||
censorshipSection.footerTitle
|
||||
= NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_FOOTER_MANUALLY_DISABLED",
|
||||
@"Table footer for the 'censorship circumvention' section shown when censorship circumvention has "
|
||||
@"been manually disabled.");
|
||||
} else {
|
||||
censorshipSection.footerTitle = NSLocalizedString(
|
||||
@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_FOOTER_AUTO_ENABLED",
|
||||
@"Table footer for the 'censorship circumvention' section shown when censorship circumvention has been "
|
||||
@"auto-enabled based on local phone number.");
|
||||
}
|
||||
} else if (isAnySocketOpen) {
|
||||
censorshipSection.footerTitle
|
||||
= NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_FOOTER_WEBSOCKET_CONNECTED",
|
||||
@"Table footer for the 'censorship circumvention' section shown when the app is connected to the "
|
||||
@"Signal service.");
|
||||
} else if (!self.reachability.isReachable) {
|
||||
censorshipSection.footerTitle
|
||||
= NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_FOOTER_NO_CONNECTION",
|
||||
@"Table footer for the 'censorship circumvention' section shown when the app is not connected to the "
|
||||
@"internet.");
|
||||
} else {
|
||||
censorshipSection.footerTitle = NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_FOOTER",
|
||||
@"Table footer for the 'censorship circumvention' section when censorship circumvention can be manually "
|
||||
@"enabled.");
|
||||
}
|
||||
|
||||
// Do enable if :
|
||||
//
|
||||
// * ...Censorship circumvention is already manually enabled (to allow users to disable it).
|
||||
//
|
||||
// Otherwise, don't enable if:
|
||||
//
|
||||
// * ...Censorship circumvention is already enabled based on the local phone number.
|
||||
// * ...The websocket is connected, since that demonstrates that no censorship is in effect.
|
||||
// * ...The internet is not reachable, since we don't want to let users to activate
|
||||
// censorship circumvention unnecessarily, e.g. if they just don't have a valid
|
||||
// internet connection.
|
||||
OWSTableSwitchBlock isCensorshipCircumventionOnBlock = ^{
|
||||
return OWSSignalService.sharedInstance.isCensorshipCircumventionActive;
|
||||
};
|
||||
Reachability *reachability = self.reachability;
|
||||
OWSTableSwitchBlock isManualCensorshipCircumventionOnEnabledBlock = ^{
|
||||
OWSSignalService *service = OWSSignalService.sharedInstance;
|
||||
if (service.isCensorshipCircumventionActive) {
|
||||
return YES;
|
||||
} else if (service.hasCensoredPhoneNumber && service.isCensorshipCircumventionManuallyDisabled) {
|
||||
return YES;
|
||||
} else if (TSSocketManager.shared.highestSocketState == OWSWebSocketStateOpen) {
|
||||
return NO;
|
||||
} else {
|
||||
return reachability.isReachable;
|
||||
}
|
||||
};
|
||||
|
||||
[censorshipSection
|
||||
addItem:[OWSTableItem switchItemWithText:NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION",
|
||||
@"Label for the 'manual censorship circumvention' switch.")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"censorship_circumvention")
|
||||
isOnBlock:isCensorshipCircumventionOnBlock
|
||||
isEnabledBlock:isManualCensorshipCircumventionOnEnabledBlock
|
||||
target:weakSelf
|
||||
selector:@selector(didToggleEnableCensorshipCircumventionSwitch:)]];
|
||||
|
||||
if (OWSSignalService.sharedInstance.isCensorshipCircumventionManuallyActivated) {
|
||||
OWSCountryMetadata *manualCensorshipCircumventionCountry =
|
||||
[weakSelf ensureManualCensorshipCircumventionCountry];
|
||||
OWSAssertDebug(manualCensorshipCircumventionCountry);
|
||||
NSString *text = [NSString
|
||||
stringWithFormat:NSLocalizedString(@"SETTINGS_ADVANCED_CENSORSHIP_CIRCUMVENTION_COUNTRY_FORMAT",
|
||||
@"Label for the 'manual censorship circumvention' country. Embeds {{the manual "
|
||||
@"censorship circumvention country}}."),
|
||||
manualCensorshipCircumventionCountry.localizedCountryName];
|
||||
[censorshipSection addItem:[OWSTableItem disclosureItemWithText:text
|
||||
actionBlock:^{
|
||||
[weakSelf showDomainFrontingCountryView];
|
||||
}]];
|
||||
}
|
||||
[contents addSection:censorshipSection];
|
||||
|
||||
self.contents = contents;
|
||||
}
|
||||
|
||||
- (void)showDomainFrontingCountryView
|
||||
{
|
||||
DomainFrontingCountryViewController *vc = [DomainFrontingCountryViewController new];
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
|
||||
- (OWSCountryMetadata *)ensureManualCensorshipCircumventionCountry
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSCountryMetadata *countryMetadata = nil;
|
||||
NSString *countryCode = OWSSignalService.sharedInstance.manualCensorshipCircumventionCountryCode;
|
||||
if (countryCode) {
|
||||
countryMetadata = [OWSCountryMetadata countryMetadataForCountryCode:countryCode];
|
||||
}
|
||||
|
||||
if (!countryMetadata) {
|
||||
countryCode = [PhoneNumber defaultCountryCode];
|
||||
if (countryCode) {
|
||||
countryMetadata = [OWSCountryMetadata countryMetadataForCountryCode:countryCode];
|
||||
}
|
||||
}
|
||||
|
||||
if (!countryMetadata) {
|
||||
countryCode = @"US";
|
||||
countryMetadata = [OWSCountryMetadata countryMetadataForCountryCode:countryCode];
|
||||
OWSAssertDebug(countryMetadata);
|
||||
}
|
||||
|
||||
if (countryMetadata) {
|
||||
// Ensure the "manual censorship circumvention" country state is in sync.
|
||||
OWSSignalService.sharedInstance.manualCensorshipCircumventionCountryCode = countryCode;
|
||||
}
|
||||
|
||||
return countryMetadata;
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)syncPushTokens
|
||||
{
|
||||
OWSSyncPushTokensJob *job =
|
||||
[[OWSSyncPushTokensJob alloc] initWithAccountManager:AppEnvironment.shared.accountManager
|
||||
preferences:Environment.shared.preferences];
|
||||
job.uploadOnlyIfStale = NO;
|
||||
[job run]
|
||||
.then(^{
|
||||
[OWSAlerts showAlertWithTitle:NSLocalizedString(@"PUSH_REGISTER_SUCCESS",
|
||||
@"Title of alert shown when push tokens sync job succeeds.")];
|
||||
})
|
||||
.catch(^(NSError *error) {
|
||||
[OWSAlerts showAlertWithTitle:NSLocalizedString(@"REGISTRATION_BODY",
|
||||
@"Title of alert shown when push tokens sync job fails.")];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)didToggleEnableLogSwitch:(UISwitch *)sender
|
||||
{
|
||||
if (!sender.isOn) {
|
||||
OWSLogInfo(@"disabling logging.");
|
||||
[[DebugLogger sharedLogger] wipeLogs];
|
||||
[[DebugLogger sharedLogger] disableFileLogging];
|
||||
} else {
|
||||
[[DebugLogger sharedLogger] enableFileLogging];
|
||||
OWSLogInfo(@"enabling logging.");
|
||||
}
|
||||
|
||||
[OWSPreferences setIsLoggingEnabled:sender.isOn];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)didToggleEnableCensorshipCircumventionSwitch:(UISwitch *)sender
|
||||
{
|
||||
OWSSignalService *service = OWSSignalService.sharedInstance;
|
||||
if (sender.isOn) {
|
||||
service.isCensorshipCircumventionManuallyDisabled = NO;
|
||||
service.isCensorshipCircumventionManuallyActivated = YES;
|
||||
} else {
|
||||
service.isCensorshipCircumventionManuallyDisabled = YES;
|
||||
service.isCensorshipCircumventionManuallyActivated = NO;
|
||||
}
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -8,39 +8,28 @@
|
|||
#import "OWSBackup.h"
|
||||
#import "OWSOrphanDataCleaner.h"
|
||||
#import "OWSScreenLockUI.h"
|
||||
#import "Pastelog.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "SignalApp.h"
|
||||
#import "SignalsNavigationController.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalCoreKit/iOSVersions.h>
|
||||
#import <SignalUtilitiesKit/AppSetup.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSNavigationController.h>
|
||||
#import <SignalUtilitiesKit/OWSPreferences.h>
|
||||
#import <SessionMessagingKit/OWSPreferences.h>
|
||||
#import <SignalUtilitiesKit/OWSProfileManager.h>
|
||||
#import <SignalUtilitiesKit/VersionMigrations.h>
|
||||
#import <SignalUtilitiesKit/AppReadiness.h>
|
||||
#import <SignalUtilitiesKit/NSUserDefaults+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWS2FAManager.h>
|
||||
#import <SignalUtilitiesKit/OWSBatchMessageProcessor.h>
|
||||
#import <SignalUtilitiesKit/OWSDisappearingMessagesJob.h>
|
||||
#import <SessionMessagingKit/AppReadiness.h>
|
||||
#import <SessionUtilitiesKit/NSUserDefaults+OWS.h>
|
||||
#import <SessionMessagingKit/OWSDisappearingMessagesJob.h>
|
||||
#import <SignalUtilitiesKit/OWSFailedAttachmentDownloadsJob.h>
|
||||
#import <SignalUtilitiesKit/OWSFailedMessagesJob.h>
|
||||
#import <SignalUtilitiesKit/OWSIncompleteCallsJob.h>
|
||||
#import <SignalUtilitiesKit/OWSMath.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageManager.h>
|
||||
#import <SignalUtilitiesKit/OWSMessageSender.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+Calling.h>
|
||||
#import <SignalUtilitiesKit/OWSReadReceiptManager.h>
|
||||
#import <SignalUtilitiesKit/SSKEnvironment.h>
|
||||
#import <SessionUtilitiesKit/OWSMath.h>
|
||||
#import <SessionMessagingKit/OWSReadReceiptManager.h>
|
||||
#import <SessionMessagingKit/SSKEnvironment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/TSAccountManager.h>
|
||||
#import <SignalUtilitiesKit/TSDatabaseView.h>
|
||||
#import <SessionMessagingKit/TSAccountManager.h>
|
||||
#import <SessionMessagingKit/TSDatabaseView.h>
|
||||
#import <SignalUtilitiesKit/TSPreKeyManager.h>
|
||||
#import <SignalUtilitiesKit/TSSocketManager.h>
|
||||
#import <YapDatabase/YapDatabaseCryptoUtils.h>
|
||||
#import <sys/utsname.h>
|
||||
|
||||
|
@ -118,20 +107,6 @@ static NSTimeInterval launchStartedAt;
|
|||
return SSKEnvironment.shared.disappearingMessagesJob;
|
||||
}
|
||||
|
||||
- (TSSocketManager *)socketManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.socketManager);
|
||||
|
||||
return SSKEnvironment.shared.socketManager;
|
||||
}
|
||||
|
||||
- (OWSMessageManager *)messageManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.messageManager);
|
||||
|
||||
return SSKEnvironment.shared.messageManager;
|
||||
}
|
||||
|
||||
- (OWSWindowManager *)windowManager
|
||||
{
|
||||
return Environment.shared.windowManager;
|
||||
|
@ -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];
|
||||
|
|
|
@ -25,18 +25,6 @@ import SignalUtilitiesKit
|
|||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public var callMessageHandler: WebRTCCallMessageHandler
|
||||
|
||||
// @objc
|
||||
// public var callService: CallService
|
||||
|
||||
// @objc
|
||||
// public var outboundCallInitiator: OutboundCallInitiator
|
||||
|
||||
@objc
|
||||
public var messageFetcherJob: MessageFetcherJob
|
||||
|
||||
@objc
|
||||
public var accountManager: AccountManager
|
||||
|
||||
|
@ -80,10 +68,6 @@ import SignalUtilitiesKit
|
|||
public var backupLazyRestore: BackupLazyRestore
|
||||
|
||||
private override init() {
|
||||
self.callMessageHandler = WebRTCCallMessageHandler()
|
||||
// self.callService = CallService()
|
||||
// self.outboundCallInitiator = OutboundCallInitiator()
|
||||
self.messageFetcherJob = MessageFetcherJob()
|
||||
self.accountManager = AccountManager()
|
||||
self.notificationPresenter = NotificationPresenter()
|
||||
self.pushRegistrationManager = PushRegistrationManager()
|
||||
|
@ -102,10 +86,7 @@ import SignalUtilitiesKit
|
|||
|
||||
@objc
|
||||
public func setup() {
|
||||
// callService.createCallUIAdapter()
|
||||
|
||||
// Hang certain singletons on SSKEnvironment too.
|
||||
SSKEnvironment.shared.notificationsManager = notificationPresenter
|
||||
// SSKEnvironment.shared.callMessageHandler = callMessageHandler
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,14 +120,6 @@ protocol NotificationPresenterAdaptee: class {
|
|||
|
||||
func cancelNotifications(threadId: String)
|
||||
func clearAllNotifications()
|
||||
|
||||
var hasReceivedSyncMessageRecently: Bool { get }
|
||||
}
|
||||
|
||||
extension NotificationPresenterAdaptee {
|
||||
var hasReceivedSyncMessageRecently: Bool {
|
||||
return OWSDeviceManager.shared().hasReceivedSyncMessage(inLastSeconds: 60)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(OWSNotificationPresenter)
|
||||
|
@ -153,10 +145,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
|
||||
// MARK: - Dependencies
|
||||
|
||||
var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
var identityManager: OWSIdentityManager {
|
||||
return OWSIdentityManager.shared()
|
||||
}
|
||||
|
@ -209,140 +197,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
return adaptee.registerNotificationSettings()
|
||||
}
|
||||
|
||||
// func presentIncomingCall(_ call: SignalCall, callerName: String) {
|
||||
//
|
||||
// let notificationTitle: String?
|
||||
// switch previewType {
|
||||
// case .noNameNoPreview:
|
||||
// notificationTitle = nil
|
||||
// case .nameNoPreview, .namePreview:
|
||||
// notificationTitle = callerName
|
||||
// }
|
||||
// let notificationBody = NotificationStrings.incomingCallBody
|
||||
//
|
||||
// let remotePhoneNumber = call.remotePhoneNumber
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber)
|
||||
//
|
||||
// guard let threadId = thread.uniqueId else {
|
||||
// owsFailDebug("threadId was unexpectedly nil")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let userInfo = [
|
||||
// AppNotificationUserInfoKey.threadId: threadId,
|
||||
// AppNotificationUserInfoKey.localCallId: call.localId.uuidString
|
||||
// ]
|
||||
//
|
||||
// DispatchQueue.main.async {
|
||||
// self.adaptee.notify(category: .incomingCall,
|
||||
// title: notificationTitle,
|
||||
// body: notificationBody,
|
||||
// userInfo: userInfo,
|
||||
// sound: .defaultiOSIncomingRingtone,
|
||||
// replacingIdentifier: call.localId.uuidString)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func presentMissedCall(_ call: SignalCall, callerName: String) {
|
||||
// let notificationTitle: String?
|
||||
// switch previewType {
|
||||
// case .noNameNoPreview:
|
||||
// notificationTitle = nil
|
||||
// case .nameNoPreview, .namePreview:
|
||||
// notificationTitle = callerName
|
||||
// }
|
||||
// let notificationBody = NotificationStrings.missedCallBody
|
||||
//
|
||||
// let remotePhoneNumber = call.remotePhoneNumber
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber)
|
||||
//
|
||||
// guard let threadId = thread.uniqueId else {
|
||||
// owsFailDebug("threadId was unexpectedly nil")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let userInfo = [
|
||||
// AppNotificationUserInfoKey.threadId: threadId,
|
||||
// AppNotificationUserInfoKey.callBackNumber: remotePhoneNumber
|
||||
// ]
|
||||
//
|
||||
// DispatchQueue.main.async {
|
||||
// let sound = self.requestSound(thread: thread)
|
||||
// self.adaptee.notify(category: .missedCall,
|
||||
// title: notificationTitle,
|
||||
// body: notificationBody,
|
||||
// userInfo: userInfo,
|
||||
// sound: sound,
|
||||
// replacingIdentifier: call.localId.uuidString)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public func presentMissedCallBecauseOfNoLongerVerifiedIdentity(call: SignalCall, callerName: String) {
|
||||
// let notificationTitle: String?
|
||||
// switch previewType {
|
||||
// case .noNameNoPreview:
|
||||
// notificationTitle = nil
|
||||
// case .nameNoPreview, .namePreview:
|
||||
// notificationTitle = callerName
|
||||
// }
|
||||
// let notificationBody = NotificationStrings.missedCallBecauseOfIdentityChangeBody
|
||||
//
|
||||
// let remotePhoneNumber = call.remotePhoneNumber
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber)
|
||||
// guard let threadId = thread.uniqueId else {
|
||||
// owsFailDebug("threadId was unexpectedly nil")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let userInfo = [
|
||||
// AppNotificationUserInfoKey.threadId: threadId
|
||||
// ]
|
||||
//
|
||||
// DispatchQueue.main.async {
|
||||
// let sound = self.requestSound(thread: thread)
|
||||
// self.adaptee.notify(category: .missedCallFromNoLongerVerifiedIdentity,
|
||||
// title: notificationTitle,
|
||||
// body: notificationBody,
|
||||
// userInfo: userInfo,
|
||||
// sound: sound,
|
||||
// replacingIdentifier: call.localId.uuidString)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public func presentMissedCallBecauseOfNewIdentity(call: SignalCall, callerName: String) {
|
||||
// let notificationTitle: String?
|
||||
// switch previewType {
|
||||
// case .noNameNoPreview:
|
||||
// notificationTitle = nil
|
||||
// case .nameNoPreview, .namePreview:
|
||||
// notificationTitle = callerName
|
||||
// }
|
||||
// let notificationBody = NotificationStrings.missedCallBecauseOfIdentityChangeBody
|
||||
//
|
||||
// let remotePhoneNumber = call.remotePhoneNumber
|
||||
// let thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber)
|
||||
//
|
||||
// guard let threadId = thread.uniqueId else {
|
||||
// owsFailDebug("threadId was unexpectedly nil")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let userInfo = [
|
||||
// AppNotificationUserInfoKey.threadId: threadId,
|
||||
// AppNotificationUserInfoKey.callBackNumber: remotePhoneNumber
|
||||
// ]
|
||||
//
|
||||
// DispatchQueue.main.async {
|
||||
// let sound = self.requestSound(thread: thread)
|
||||
// self.adaptee.notify(category: .missedCall,
|
||||
// title: notificationTitle,
|
||||
// body: notificationBody,
|
||||
// userInfo: userInfo,
|
||||
// sound: sound,
|
||||
// replacingIdentifier: call.localId.uuidString)
|
||||
// }
|
||||
// }
|
||||
|
||||
public func notifyUser(for incomingMessage: TSIncomingMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) {
|
||||
|
||||
guard !thread.isMuted else {
|
||||
|
@ -359,7 +213,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
// for more details.
|
||||
let messageText = DisplayableText.filterNotificationText(rawMessageText)
|
||||
|
||||
let senderName = OWSUserProfile.fetch(uniqueId: incomingMessage.authorId, transaction: transaction)?.profileName ?? contactsManager.displayName(forPhoneIdentifier: incomingMessage.authorId)
|
||||
let senderName = SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: incomingMessage.authorId, avoidingWriteTransaction: true) ?? incomingMessage.authorId
|
||||
|
||||
let notificationTitle: String?
|
||||
switch previewType {
|
||||
|
@ -401,12 +255,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
|
|||
// Don't reply from lockscreen if anyone in this conversation is
|
||||
// "no longer verified".
|
||||
var category = AppNotificationCategory.incomingMessage
|
||||
for recipientId in thread.recipientIdentifiers {
|
||||
if self.identityManager.verificationState(forRecipientId: recipientId) == .noLongerVerified {
|
||||
category = AppNotificationCategory.incomingMessageFromNoLongerVerifiedIdentity
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let userInfo = [
|
||||
AppNotificationUserInfoKey.threadId: threadId
|
||||
|
@ -554,14 +402,6 @@ class NotificationActionHandler {
|
|||
return SignalApp.shared()
|
||||
}
|
||||
|
||||
var messageSender: MessageSender {
|
||||
return SSKEnvironment.shared.messageSender
|
||||
}
|
||||
|
||||
// var callUIAdapter: CallUIAdapter {
|
||||
// return AppEnvironment.shared.callService.callUIAdapter
|
||||
// }
|
||||
|
||||
var notificationPresenter: NotificationPresenter {
|
||||
return AppEnvironment.shared.notificationPresenter
|
||||
}
|
||||
|
@ -572,41 +412,6 @@ class NotificationActionHandler {
|
|||
|
||||
// MARK: -
|
||||
|
||||
// func answerCall(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
|
||||
// guard let localCallIdString = userInfo[AppNotificationUserInfoKey.localCallId] as? String else {
|
||||
// throw NotificationError.failDebug("localCallIdString was unexpectedly nil")
|
||||
// }
|
||||
//
|
||||
// guard let localCallId = UUID(uuidString: localCallIdString) else {
|
||||
// throw NotificationError.failDebug("unable to build localCallId. localCallIdString: \(localCallIdString)")
|
||||
// }
|
||||
//
|
||||
// callUIAdapter.answerCall(localId: localCallId)
|
||||
// return Promise.value(())
|
||||
// }
|
||||
//
|
||||
// func callBack(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
|
||||
// guard let recipientId = userInfo[AppNotificationUserInfoKey.callBackNumber] as? String else {
|
||||
// throw NotificationError.failDebug("recipientId was unexpectedly nil")
|
||||
// }
|
||||
//
|
||||
// callUIAdapter.startAndShowOutgoingCall(recipientId: recipientId, hasLocalVideo: false)
|
||||
// return Promise.value(())
|
||||
// }
|
||||
//
|
||||
// func declineCall(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
|
||||
// guard let localCallIdString = userInfo[AppNotificationUserInfoKey.localCallId] as? String else {
|
||||
// throw NotificationError.failDebug("localCallIdString was unexpectedly nil")
|
||||
// }
|
||||
//
|
||||
// guard let localCallId = UUID(uuidString: localCallIdString) else {
|
||||
// throw NotificationError.failDebug("unable to build localCallId. localCallIdString: \(localCallIdString)")
|
||||
// }
|
||||
//
|
||||
// callUIAdapter.declineCall(localId: localCallId)
|
||||
// return Promise.value(())
|
||||
// }
|
||||
|
||||
func markAsRead(userInfo: [AnyHashable: Any]) throws -> Promise<Void> {
|
||||
guard let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else {
|
||||
throw NotificationError.failDebug("threadId was unexpectedly nil")
|
||||
|
@ -629,15 +434,21 @@ class NotificationActionHandler {
|
|||
}
|
||||
|
||||
return markAsRead(thread: thread).then { () -> Promise<Void> in
|
||||
let sendPromise = ThreadUtil.sendMessageNonDurably(text: replyText,
|
||||
thread: thread,
|
||||
quotedReplyModel: nil,
|
||||
messageSender: self.messageSender)
|
||||
|
||||
return sendPromise.recover { error in
|
||||
Logger.warn("Failed to send reply message from notification with error: \(error)")
|
||||
self.notificationPresenter.notifyForFailedSend(inThread: thread)
|
||||
let message = VisibleMessage()
|
||||
message.sentTimestamp = NSDate.millisecondTimestamp()
|
||||
message.text = replyText
|
||||
let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread)
|
||||
Storage.write { transaction in
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
var promise: Promise<Void>!
|
||||
Storage.writeSync { transaction in
|
||||
promise = MessageSender.sendNonDurably(message, in: thread, using: transaction)
|
||||
}
|
||||
promise.catch { [weak self] error in
|
||||
self?.notificationPresenter.notifyForFailedSend(inThread: thread)
|
||||
}
|
||||
return promise
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -666,25 +477,6 @@ class NotificationActionHandler {
|
|||
}
|
||||
}
|
||||
|
||||
extension ThreadUtil {
|
||||
static var dbReadConnection: YapDatabaseConnection {
|
||||
return OWSPrimaryStorage.shared().dbReadConnection
|
||||
}
|
||||
|
||||
class func sendMessageNonDurably(text: String, thread: TSThread, quotedReplyModel: OWSQuotedReplyModel?, messageSender: MessageSender) -> Promise<Void> {
|
||||
return Promise { resolver in
|
||||
self.dbReadConnection.read { transaction in
|
||||
_ = self.sendMessageNonDurably(withText: text,
|
||||
in: thread,
|
||||
quotedReplyModel: quotedReplyModel,
|
||||
transaction: transaction,
|
||||
messageSender: messageSender,
|
||||
completion: resolver.resolve)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum NotificationError: Error {
|
||||
case assertionError(description: String)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ public class AvatarTableViewCell: UITableViewCell {
|
|||
|
||||
private let columns: UIStackView
|
||||
private let textRows: UIStackView
|
||||
private let avatarView: AvatarImageView
|
||||
// private let avatarView: AvatarImageView
|
||||
|
||||
private let _textLabel: UILabel
|
||||
override public var textLabel: UILabel? {
|
||||
|
@ -27,8 +27,8 @@ public class AvatarTableViewCell: UITableViewCell {
|
|||
|
||||
@objc
|
||||
public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
self.avatarView = AvatarImageView()
|
||||
avatarView.autoSetDimensions(to: CGSize(width: CGFloat(kStandardAvatarSize), height: CGFloat(kStandardAvatarSize)))
|
||||
// self.avatarView = AvatarImageView()
|
||||
// avatarView.autoSetDimensions(to: CGSize(width: CGFloat(kStandardAvatarSize), height: CGFloat(kStandardAvatarSize)))
|
||||
|
||||
self._textLabel = UILabel()
|
||||
self._detailTextLabel = UILabel()
|
||||
|
@ -36,7 +36,7 @@ public class AvatarTableViewCell: UITableViewCell {
|
|||
self.textRows = UIStackView(arrangedSubviews: [_textLabel, _detailTextLabel])
|
||||
textRows.axis = .vertical
|
||||
|
||||
self.columns = UIStackView(arrangedSubviews: [avatarView, textRows])
|
||||
self.columns = UIStackView(arrangedSubviews: [ textRows ])
|
||||
columns.axis = .horizontal
|
||||
columns.spacing = CGFloat(kContactCellAvatarTextMargin)
|
||||
|
||||
|
@ -54,7 +54,7 @@ public class AvatarTableViewCell: UITableViewCell {
|
|||
|
||||
@objc
|
||||
public func configure(image: UIImage?, text: String?, detailText: String?) {
|
||||
self.avatarView.image = image
|
||||
// self.avatarView.image = image
|
||||
self.textLabel?.text = text
|
||||
self.detailTextLabel?.text = detailText
|
||||
|
||||
|
@ -65,7 +65,7 @@ public class AvatarTableViewCell: UITableViewCell {
|
|||
public override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
self.avatarView.image = nil
|
||||
// self.avatarView.image = nil
|
||||
self.textLabel?.text = nil
|
||||
self.detailTextLabel?.text = nil
|
||||
}
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
#import "OWSNavigationController.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <MobileCoreServices/UTCoreTypes.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/PhoneNumber.h>
|
||||
#import <SignalUtilitiesKit/TSGroupModel.h>
|
||||
#import <SignalUtilitiesKit/TSGroupThread.h>
|
||||
#import <SignalUtilitiesKit/TSThread.h>
|
||||
|
||||
#import <SessionMessagingKit/TSGroupModel.h>
|
||||
#import <SessionMessagingKit/TSGroupThread.h>
|
||||
#import <SessionMessagingKit/TSThread.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
@ -36,14 +36,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
[actionSheet addAction:[OWSAlerts cancelAction]];
|
||||
|
||||
// UIAlertAction *takePictureAction = [UIAlertAction
|
||||
// actionWithTitle:NSLocalizedString(@"MEDIA_FROM_CAMERA_BUTTON", @"media picker option to take photo or video")
|
||||
// style:UIAlertActionStyleDefault
|
||||
// handler:^(UIAlertAction *_Nonnull action) {
|
||||
// [self takePicture];
|
||||
// }];
|
||||
// [actionSheet addAction:takePictureAction];
|
||||
|
||||
UIAlertAction *choosePictureAction = [UIAlertAction
|
||||
actionWithTitle:NSLocalizedString(@"MEDIA_FROM_LIBRARY_BUTTON", @"media picker option to choose from library")
|
||||
style:UIAlertActionStyleDefault
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface BlockListViewController : OWSViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,175 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BlockListViewController.h"
|
||||
#import "AddToBlockListViewController.h"
|
||||
#import "BlockListUIUtils.h"
|
||||
#import "ContactTableViewCell.h"
|
||||
#import "ContactsViewHelper.h"
|
||||
#import "OWSTableViewController.h"
|
||||
#import "PhoneNumber.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/OWSBlockingManager.h>
|
||||
#import <SignalUtilitiesKit/TSGroupThread.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface BlockListViewController () <ContactsViewHelperDelegate>
|
||||
|
||||
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
|
||||
|
||||
@property (nonatomic, readonly) OWSTableViewController *tableViewController;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation BlockListViewController
|
||||
|
||||
- (OWSBlockingManager *)blockingManager
|
||||
{
|
||||
return OWSBlockingManager.sharedManager;
|
||||
}
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
_contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
|
||||
|
||||
self.title
|
||||
= NSLocalizedString(@"SETTINGS_BLOCK_LIST_TITLE", @"Label for the block list section of the settings view");
|
||||
|
||||
_tableViewController = [OWSTableViewController new];
|
||||
[self.view addSubview:self.tableViewController.view];
|
||||
[self addChildViewController:self.tableViewController];
|
||||
[_tableViewController.view autoPinEdgesToSuperviewEdges];
|
||||
self.tableViewController.tableView.rowHeight = UITableViewAutomaticDimension;
|
||||
self.tableViewController.tableView.estimatedRowHeight = 60;
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - Table view data source
|
||||
|
||||
- (void)updateTableContents
|
||||
{
|
||||
OWSTableContents *contents = [OWSTableContents new];
|
||||
|
||||
__weak BlockListViewController *weakSelf = self;
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
|
||||
// "Add" section
|
||||
|
||||
OWSTableSection *addSection = [OWSTableSection new];
|
||||
addSection.footerTitle = NSLocalizedString(
|
||||
@"BLOCK_USER_BEHAVIOR_EXPLANATION", @"An explanation of the consequences of blocking another user.");
|
||||
|
||||
[addSection
|
||||
addItem:[OWSTableItem
|
||||
disclosureItemWithText:NSLocalizedString(@"SETTINGS_BLOCK_LIST_ADD_BUTTON",
|
||||
@"A label for the 'add phone number' button in the block list table.")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"add")
|
||||
actionBlock:^{
|
||||
AddToBlockListViewController *vc = [[AddToBlockListViewController alloc] init];
|
||||
[weakSelf.navigationController pushViewController:vc animated:YES];
|
||||
}]];
|
||||
[contents addSection:addSection];
|
||||
|
||||
// "Blocklist" section
|
||||
|
||||
NSArray<NSString *> *blockedPhoneNumbers =
|
||||
[self.blockingManager.blockedPhoneNumbers sortedArrayUsingSelector:@selector(compare:)];
|
||||
|
||||
if (blockedPhoneNumbers.count > 0) {
|
||||
OWSTableSection *blockedContactsSection = [OWSTableSection new];
|
||||
blockedContactsSection.headerTitle = NSLocalizedString(
|
||||
@"BLOCK_LIST_BLOCKED_USERS_SECTION", @"Section header for users that have been blocked");
|
||||
|
||||
for (NSString *phoneNumber in blockedPhoneNumbers) {
|
||||
[blockedContactsSection addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
ContactTableViewCell *cell = [ContactTableViewCell new];
|
||||
[cell configureWithRecipientId:phoneNumber];
|
||||
cell.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(
|
||||
BlockListViewController, @"user");
|
||||
return cell;
|
||||
}
|
||||
customRowHeight:UITableViewAutomaticDimension
|
||||
actionBlock:^{
|
||||
[BlockListUIUtils
|
||||
showUnblockPhoneNumberActionSheet:phoneNumber
|
||||
fromViewController:weakSelf
|
||||
blockingManager:helper.blockingManager
|
||||
contactsManager:helper.contactsManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
[weakSelf updateTableContents];
|
||||
}];
|
||||
}]];
|
||||
}
|
||||
[contents addSection:blockedContactsSection];
|
||||
}
|
||||
|
||||
NSArray<TSGroupModel *> *blockedGroups = self.blockingManager.blockedGroups;
|
||||
if (blockedGroups.count > 0) {
|
||||
OWSTableSection *blockedGroupsSection = [OWSTableSection new];
|
||||
blockedGroupsSection.headerTitle = NSLocalizedString(
|
||||
@"BLOCK_LIST_BLOCKED_GROUPS_SECTION", @"Section header for groups that have been blocked");
|
||||
|
||||
for (TSGroupModel *blockedGroup in blockedGroups) {
|
||||
UIImage *_Nullable image = blockedGroup.groupImage;
|
||||
if (!image) {
|
||||
NSString *conversationColorName =
|
||||
[TSGroupThread defaultConversationColorNameForGroupId:blockedGroup.groupId];
|
||||
image = [OWSGroupAvatarBuilder defaultAvatarForGroupId:blockedGroup.groupId
|
||||
conversationColorName:conversationColorName
|
||||
diameter:kStandardAvatarSize];
|
||||
}
|
||||
NSString *groupName
|
||||
= blockedGroup.groupName.length > 0 ? blockedGroup.groupName : TSGroupThread.defaultGroupName;
|
||||
|
||||
[blockedGroupsSection addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
OWSAvatarTableViewCell *cell = [OWSAvatarTableViewCell new];
|
||||
[cell configureWithImage:image
|
||||
text:groupName
|
||||
detailText:nil];
|
||||
return cell;
|
||||
}
|
||||
customRowHeight:UITableViewAutomaticDimension
|
||||
actionBlock:^{
|
||||
[BlockListUIUtils showUnblockGroupActionSheet:blockedGroup
|
||||
displayName:groupName
|
||||
fromViewController:weakSelf
|
||||
blockingManager:helper.blockingManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
[weakSelf updateTableContents];
|
||||
}];
|
||||
}]];
|
||||
}
|
||||
[contents addSection:blockedGroupsSection];
|
||||
}
|
||||
|
||||
self.tableViewController.contents = contents;
|
||||
}
|
||||
|
||||
#pragma mark - ContactsViewHelperDelegate
|
||||
|
||||
- (void)contactsViewHelperDidUpdateContacts
|
||||
{
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (BOOL)shouldHideLocalNumber
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,549 +0,0 @@
|
|||
////
|
||||
//// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
////
|
||||
//
|
||||
//import Foundation
|
||||
//import AVFoundation
|
||||
//import SignalUtilitiesKit
|
||||
//import SignalUtilitiesKit
|
||||
//
|
||||
//struct AudioSource: Hashable {
|
||||
//
|
||||
// let image: UIImage
|
||||
// let localizedName: String
|
||||
// let portDescription: AVAudioSessionPortDescription?
|
||||
//
|
||||
// // The built-in loud speaker / aka speakerphone
|
||||
// let isBuiltInSpeaker: Bool
|
||||
//
|
||||
// // The built-in quiet speaker, aka the normal phone handset receiver earpiece
|
||||
// let isBuiltInEarPiece: Bool
|
||||
//
|
||||
// init(localizedName: String, image: UIImage, isBuiltInSpeaker: Bool, isBuiltInEarPiece: Bool, portDescription: AVAudioSessionPortDescription? = nil) {
|
||||
// self.localizedName = localizedName
|
||||
// self.image = image
|
||||
// self.isBuiltInSpeaker = isBuiltInSpeaker
|
||||
// self.isBuiltInEarPiece = isBuiltInEarPiece
|
||||
// self.portDescription = portDescription
|
||||
// }
|
||||
//
|
||||
// init(portDescription: AVAudioSessionPortDescription) {
|
||||
//
|
||||
// let isBuiltInEarPiece = portDescription.portType == AVAudioSession.Port.builtInMic
|
||||
//
|
||||
// // portDescription.portName works well for BT linked devices, but if we are using
|
||||
// // the built in mic, we have "iPhone Microphone" which is a little awkward.
|
||||
// // In that case, instead we prefer just the model name e.g. "iPhone" or "iPad"
|
||||
// let localizedName = isBuiltInEarPiece ? UIDevice.current.localizedModel : portDescription.portName
|
||||
//
|
||||
// self.init(localizedName: localizedName,
|
||||
// image: #imageLiteral(resourceName: "button_phone_white"), // TODO
|
||||
// isBuiltInSpeaker: false,
|
||||
// isBuiltInEarPiece: isBuiltInEarPiece,
|
||||
// portDescription: portDescription)
|
||||
// }
|
||||
//
|
||||
// // Speakerphone is handled separately from the other audio routes as it doesn't appear as an "input"
|
||||
// static var builtInSpeaker: AudioSource {
|
||||
// return self.init(localizedName: NSLocalizedString("AUDIO_ROUTE_BUILT_IN_SPEAKER", comment: "action sheet button title to enable built in speaker during a call"),
|
||||
// image: #imageLiteral(resourceName: "button_phone_white"), //TODO
|
||||
// isBuiltInSpeaker: true,
|
||||
// isBuiltInEarPiece: false)
|
||||
// }
|
||||
//
|
||||
// // MARK: Hashable
|
||||
//
|
||||
// static func ==(lhs: AudioSource, rhs: AudioSource) -> Bool {
|
||||
// // Simply comparing the `portDescription` vs the `portDescription.uid`
|
||||
// // caused multiple instances of the built in mic to turn up in a set.
|
||||
// if lhs.isBuiltInSpeaker && rhs.isBuiltInSpeaker {
|
||||
// return true
|
||||
// }
|
||||
//
|
||||
// if lhs.isBuiltInSpeaker || rhs.isBuiltInSpeaker {
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// guard let lhsPortDescription = lhs.portDescription else {
|
||||
// owsFailDebug("only the built in speaker should lack a port description")
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// guard let rhsPortDescription = rhs.portDescription else {
|
||||
// owsFailDebug("only the built in speaker should lack a port description")
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// return lhsPortDescription.uid == rhsPortDescription.uid
|
||||
// }
|
||||
//
|
||||
// var hashValue: Int {
|
||||
// guard let portDescription = self.portDescription else {
|
||||
// assert(self.isBuiltInSpeaker)
|
||||
// return "Built In Speaker".hashValue
|
||||
// }
|
||||
// return portDescription.uid.hash
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//protocol CallAudioServiceDelegate: class {
|
||||
// func callAudioService(_ callAudioService: CallAudioService, didUpdateIsSpeakerphoneEnabled isEnabled: Bool)
|
||||
// func callAudioServiceDidChangeAudioSession(_ callAudioService: CallAudioService)
|
||||
//}
|
||||
//
|
||||
//@objc class CallAudioService: NSObject, CallObserver {
|
||||
//
|
||||
// private var vibrateTimer: Timer?
|
||||
// private let audioPlayer = AVAudioPlayer()
|
||||
// private let handleRinging: Bool
|
||||
// weak var delegate: CallAudioServiceDelegate? {
|
||||
// willSet {
|
||||
// assert(newValue == nil || delegate == nil)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // MARK: Vibration config
|
||||
// private let vibrateRepeatDuration = 1.6
|
||||
//
|
||||
// // Our ring buzz is a pair of vibrations.
|
||||
// // `pulseDuration` is the small pause between the two vibrations in the pair.
|
||||
// private let pulseDuration = 0.2
|
||||
//
|
||||
// var audioSession: OWSAudioSession {
|
||||
// return Environment.shared.audioSession
|
||||
// }
|
||||
//
|
||||
// var avAudioSession: AVAudioSession {
|
||||
// return AVAudioSession.sharedInstance()
|
||||
// }
|
||||
//
|
||||
// // MARK: - Initializers
|
||||
//
|
||||
// init(handleRinging: Bool) {
|
||||
// self.handleRinging = handleRinging
|
||||
//
|
||||
// super.init()
|
||||
//
|
||||
// // We cannot assert singleton here, because this class gets rebuilt when the user changes relevant call settings
|
||||
//
|
||||
// // Configure audio session so we don't prompt user with Record permission until call is connected.
|
||||
//
|
||||
// audioSession.configureRTCAudio()
|
||||
// NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: avAudioSession, queue: nil) { _ in
|
||||
// assert(!Thread.isMainThread)
|
||||
// self.updateIsSpeakerphoneEnabled()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// deinit {
|
||||
// NotificationCenter.default.removeObserver(self)
|
||||
// }
|
||||
//
|
||||
// // MARK: - CallObserver
|
||||
//
|
||||
// internal func stateDidChange(call: SignalCall, state: CallState) {
|
||||
// AssertIsOnMainThread()
|
||||
// self.handleState(call: call)
|
||||
// }
|
||||
//
|
||||
// internal func muteDidChange(call: SignalCall, isMuted: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// ensureProperAudioSession(call: call)
|
||||
// }
|
||||
//
|
||||
// internal func holdDidChange(call: SignalCall, isOnHold: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// ensureProperAudioSession(call: call)
|
||||
// }
|
||||
//
|
||||
// internal func audioSourceDidChange(call: SignalCall, audioSource: AudioSource?) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// ensureProperAudioSession(call: call)
|
||||
//
|
||||
// if let audioSource = audioSource, audioSource.isBuiltInSpeaker {
|
||||
// self.isSpeakerphoneEnabled = true
|
||||
// } else {
|
||||
// self.isSpeakerphoneEnabled = false
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// internal func hasLocalVideoDidChange(call: SignalCall, hasLocalVideo: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// ensureProperAudioSession(call: call)
|
||||
// }
|
||||
//
|
||||
// // Speakerphone can be manipulated by the in-app callscreen or via the system callscreen (CallKit).
|
||||
// // Unlike other CallKit CallScreen buttons, enabling doesn't trigger a CXAction, so it's not as simple
|
||||
// // to track state changes. Instead we never store the state and directly access the ground-truth in the
|
||||
// // AVAudioSession.
|
||||
// private(set) var isSpeakerphoneEnabled: Bool = false {
|
||||
// didSet {
|
||||
// self.delegate?.callAudioService(self, didUpdateIsSpeakerphoneEnabled: isSpeakerphoneEnabled)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public func requestSpeakerphone(isEnabled: Bool) {
|
||||
// // This is a little too slow to execute on the main thread and the results are not immediately available after execution
|
||||
// // anyway, so we dispatch async. If you need to know the new value, you'll need to check isSpeakerphoneEnabled and take
|
||||
// // advantage of the CallAudioServiceDelegate.callAudioService(_:didUpdateIsSpeakerphoneEnabled:)
|
||||
// DispatchQueue.global().async {
|
||||
// do {
|
||||
// try self.avAudioSession.overrideOutputAudioPort( isEnabled ? .speaker : .none )
|
||||
// } catch {
|
||||
// owsFailDebug("failed to set \(#function) = \(isEnabled) with error: \(error)")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func updateIsSpeakerphoneEnabled() {
|
||||
// let value = avAudioSession.currentRoute.outputs.contains { (portDescription: AVAudioSessionPortDescription) -> Bool in
|
||||
// return portDescription.portType == .builtInSpeaker
|
||||
// }
|
||||
// DispatchQueue.main.async {
|
||||
// self.isSpeakerphoneEnabled = value
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func ensureProperAudioSession(call: SignalCall?) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// guard let call = call, !call.isTerminated else {
|
||||
// // Revert to default audio
|
||||
// setAudioSession(category: .soloAmbient,
|
||||
// mode: .default)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Disallow bluetooth while (and only while) the user has explicitly chosen the built in receiver.
|
||||
// //
|
||||
// // NOTE: I'm actually not sure why this is required - it seems like we should just be able
|
||||
// // to setPreferredInput to call.audioSource.portDescription in this case,
|
||||
// // but in practice I'm seeing the call revert to the bluetooth headset.
|
||||
// // Presumably something else (in WebRTC?) is touching our shared AudioSession. - mjk
|
||||
// let options: AVAudioSession.CategoryOptions = call.audioSource?.isBuiltInEarPiece == true ? [] : [.allowBluetooth]
|
||||
//
|
||||
// if call.state == .localRinging {
|
||||
// // SoloAmbient plays through speaker, but respects silent switch
|
||||
// setAudioSession(category: .soloAmbient,
|
||||
// mode: .default)
|
||||
// } else if call.hasLocalVideo {
|
||||
// // Because ModeVideoChat affects gain, we don't want to apply it until the call is connected.
|
||||
// // otherwise sounds like ringing will be extra loud for video vs. speakerphone
|
||||
//
|
||||
// // Apple Docs say that setting mode to AVAudioSessionModeVideoChat has the
|
||||
// // side effect of setting options: .allowBluetooth, when I remove the (seemingly unnecessary)
|
||||
// // option, and inspect AVAudioSession.sharedInstance.categoryOptions == 0. And availableInputs
|
||||
// // does not include my linked bluetooth device
|
||||
// setAudioSession(category: .playAndRecord,
|
||||
// mode: .videoChat,
|
||||
// options: options)
|
||||
// } else {
|
||||
// // Apple Docs say that setting mode to AVAudioSessionModeVoiceChat has the
|
||||
// // side effect of setting options: .allowBluetooth, when I remove the (seemingly unnecessary)
|
||||
// // option, and inspect AVAudioSession.sharedInstance.categoryOptions == 0. And availableInputs
|
||||
// // does not include my linked bluetooth device
|
||||
// setAudioSession(category: .playAndRecord,
|
||||
// mode: .voiceChat,
|
||||
// options: options)
|
||||
// }
|
||||
//
|
||||
// do {
|
||||
// // It's important to set preferred input *after* ensuring properAudioSession
|
||||
// // because some sources are only valid for certain category/option combinations.
|
||||
// let existingPreferredInput = avAudioSession.preferredInput
|
||||
// if existingPreferredInput != call.audioSource?.portDescription {
|
||||
// Logger.info("changing preferred input: \(String(describing: existingPreferredInput)) -> \(String(describing: call.audioSource?.portDescription))")
|
||||
// try avAudioSession.setPreferredInput(call.audioSource?.portDescription)
|
||||
// }
|
||||
//
|
||||
// } catch {
|
||||
// owsFailDebug("failed setting audio source with error: \(error) isSpeakerPhoneEnabled: \(call.isSpeakerphoneEnabled)")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // MARK: - Service action handlers
|
||||
//
|
||||
// public func didUpdateVideoTracks(call: SignalCall?) {
|
||||
// Logger.verbose("")
|
||||
//
|
||||
// self.ensureProperAudioSession(call: call)
|
||||
// }
|
||||
//
|
||||
// public func handleState(call: SignalCall) {
|
||||
// assert(Thread.isMainThread)
|
||||
//
|
||||
// Logger.verbose("new state: \(call.state)")
|
||||
//
|
||||
// // Stop playing sounds while switching audio session so we don't
|
||||
// // get any blips across a temporary unintended route.
|
||||
// stopPlayingAnySounds()
|
||||
// self.ensureProperAudioSession(call: call)
|
||||
//
|
||||
// switch call.state {
|
||||
// case .idle: handleIdle(call: call)
|
||||
// case .dialing: handleDialing(call: call)
|
||||
// case .answering: handleAnswering(call: call)
|
||||
// case .remoteRinging: handleRemoteRinging(call: call)
|
||||
// case .localRinging: handleLocalRinging(call: call)
|
||||
// case .connected: handleConnected(call: call)
|
||||
// case .reconnecting: handleReconnecting(call: call)
|
||||
// case .localFailure: handleLocalFailure(call: call)
|
||||
// case .localHangup: handleLocalHangup(call: call)
|
||||
// case .remoteHangup: handleRemoteHangup(call: call)
|
||||
// case .remoteBusy: handleBusy(call: call)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func handleIdle(call: SignalCall) {
|
||||
// Logger.debug("")
|
||||
// }
|
||||
//
|
||||
// private func handleDialing(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// // HACK: Without this async, dialing sound only plays once. I don't really understand why. Does the audioSession
|
||||
// // need some time to settle? Is somethign else interrupting our session?
|
||||
// DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.2) {
|
||||
// self.play(sound: OWSSound.callConnecting)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func handleAnswering(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
// }
|
||||
//
|
||||
// private func handleRemoteRinging(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// self.play(sound: OWSSound.callOutboundRinging)
|
||||
// }
|
||||
//
|
||||
// private func handleLocalRinging(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// startRinging(call: call)
|
||||
// }
|
||||
//
|
||||
// private func handleConnected(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
// }
|
||||
//
|
||||
// private func handleReconnecting(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
// }
|
||||
//
|
||||
// private func handleLocalFailure(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// play(sound: OWSSound.callFailure)
|
||||
// handleCallEnded(call: call)
|
||||
// }
|
||||
//
|
||||
// private func handleLocalHangup(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// handleCallEnded(call: call)
|
||||
// }
|
||||
//
|
||||
// private func handleRemoteHangup(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// vibrate()
|
||||
//
|
||||
// handleCallEnded(call: call)
|
||||
// }
|
||||
//
|
||||
// private func handleBusy(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// play(sound: OWSSound.callBusy)
|
||||
//
|
||||
// // Let the busy sound play for 4 seconds. The full file is longer than necessary
|
||||
// DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 4.0) {
|
||||
// self.handleCallEnded(call: call)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func handleCallEnded(call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// // Stop solo audio, revert to default.
|
||||
// isSpeakerphoneEnabled = false
|
||||
// setAudioSession(category: .soloAmbient)
|
||||
// }
|
||||
//
|
||||
// // MARK: Playing Sounds
|
||||
//
|
||||
// var currentPlayer: OWSAudioPlayer?
|
||||
//
|
||||
// private func stopPlayingAnySounds() {
|
||||
// currentPlayer?.stop()
|
||||
// stopAnyRingingVibration()
|
||||
// }
|
||||
//
|
||||
// private func play(sound: OWSSound) {
|
||||
// guard let newPlayer = OWSSounds.audioPlayer(for: sound, audioBehavior: .call) else {
|
||||
// owsFailDebug("unable to build player for sound: \(OWSSounds.displayName(for: sound))")
|
||||
// return
|
||||
// }
|
||||
// Logger.info("playing sound: \(OWSSounds.displayName(for: sound))")
|
||||
//
|
||||
// // It's important to stop the current player **before** starting the new player. In the case that
|
||||
// // we're playing the same sound, since the player is memoized on the sound instance, we'd otherwise
|
||||
// // stop the sound we just started.
|
||||
// self.currentPlayer?.stop()
|
||||
// newPlayer.play()
|
||||
// self.currentPlayer = newPlayer
|
||||
// }
|
||||
//
|
||||
// // MARK: - Ringing
|
||||
//
|
||||
// private func startRinging(call: SignalCall) {
|
||||
// guard handleRinging else {
|
||||
// Logger.debug("ignoring \(#function) since CallKit handles it's own ringing state")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// vibrateTimer = WeakTimer.scheduledTimer(timeInterval: vibrateRepeatDuration, target: self, userInfo: nil, repeats: true) {[weak self] _ in
|
||||
// self?.ringVibration()
|
||||
// }
|
||||
// vibrateTimer?.fire()
|
||||
// play(sound: .defaultiOSIncomingRingtone)
|
||||
// }
|
||||
//
|
||||
// private func stopAnyRingingVibration() {
|
||||
// guard handleRinging else {
|
||||
// Logger.debug("ignoring \(#function) since CallKit handles it's own ringing state")
|
||||
// return
|
||||
// }
|
||||
// Logger.debug("")
|
||||
//
|
||||
// // Stop vibrating
|
||||
// vibrateTimer?.invalidate()
|
||||
// vibrateTimer = nil
|
||||
// }
|
||||
//
|
||||
// // public so it can be called by timer via selector
|
||||
// public func ringVibration() {
|
||||
// // Since a call notification is more urgent than a message notifaction, we
|
||||
// // vibrate twice, like a pulse, to differentiate from a normal notification vibration.
|
||||
// vibrate()
|
||||
// DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + pulseDuration) {
|
||||
// self.vibrate()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func vibrate() {
|
||||
// // TODO implement HapticAdapter for iPhone7 and up
|
||||
// AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
|
||||
// }
|
||||
//
|
||||
// // MARK: - AudioSession MGMT
|
||||
// // TODO move this to CallAudioSession?
|
||||
//
|
||||
// // Note this method is sensitive to the current audio session configuration.
|
||||
// // Specifically if you call it while speakerphone is enabled you won't see
|
||||
// // any connected bluetooth routes.
|
||||
// var availableInputs: [AudioSource] {
|
||||
// guard let availableInputs = avAudioSession.availableInputs else {
|
||||
// // I'm not sure why this would happen, but it may indicate an error.
|
||||
// owsFailDebug("No available inputs or inputs not ready")
|
||||
// return [AudioSource.builtInSpeaker]
|
||||
// }
|
||||
//
|
||||
// Logger.info("availableInputs: \(availableInputs)")
|
||||
// return [AudioSource.builtInSpeaker] + availableInputs.map { portDescription in
|
||||
// return AudioSource(portDescription: portDescription)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func currentAudioSource(call: SignalCall) -> AudioSource? {
|
||||
// if let audioSource = call.audioSource {
|
||||
// return audioSource
|
||||
// }
|
||||
//
|
||||
// // Before the user has specified an audio source on the call, we rely on the existing
|
||||
// // system state to determine the current audio source.
|
||||
// // If a bluetooth is connected, this will be bluetooth, otherwise
|
||||
// // this will be the receiver.
|
||||
// guard let portDescription = avAudioSession.currentRoute.inputs.first else {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// return AudioSource(portDescription: portDescription)
|
||||
// }
|
||||
//
|
||||
// private func setAudioSession(category: AVAudioSession.Category,
|
||||
// mode: AVAudioSession.Mode? = nil,
|
||||
// options: AVAudioSession.CategoryOptions = AVAudioSession.CategoryOptions(rawValue: 0)) {
|
||||
//
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// var audioSessionChanged = false
|
||||
// do {
|
||||
// if #available(iOS 10.0, *), let mode = mode {
|
||||
// let oldCategory = avAudioSession.category
|
||||
// let oldMode = avAudioSession.mode
|
||||
// let oldOptions = avAudioSession.categoryOptions
|
||||
//
|
||||
// guard oldCategory != category || oldMode != mode || oldOptions != options else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// audioSessionChanged = true
|
||||
//
|
||||
// if oldCategory != category {
|
||||
// Logger.debug("audio session changed category: \(oldCategory) -> \(category) ")
|
||||
// }
|
||||
// if oldMode != mode {
|
||||
// Logger.debug("audio session changed mode: \(oldMode) -> \(mode) ")
|
||||
// }
|
||||
// if oldOptions != options {
|
||||
// Logger.debug("audio session changed options: \(oldOptions) -> \(options) ")
|
||||
// }
|
||||
// try avAudioSession.setCategory(category, mode: mode, options: options)
|
||||
//
|
||||
// } else {
|
||||
// let oldCategory = avAudioSession.category
|
||||
// let oldOptions = avAudioSession.categoryOptions
|
||||
//
|
||||
// guard avAudioSession.category != category || avAudioSession.categoryOptions != options else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// audioSessionChanged = true
|
||||
//
|
||||
// if oldCategory != category {
|
||||
// Logger.debug("audio session changed category: \(oldCategory) -> \(category) ")
|
||||
// }
|
||||
// if oldOptions != options {
|
||||
// Logger.debug("audio session changed options: \(oldOptions) -> \(options) ")
|
||||
// }
|
||||
// try avAudioSession.ows_setCategory(category, with: options)
|
||||
// }
|
||||
// } catch {
|
||||
// let message = "failed to set category: \(category) mode: \(String(describing: mode)), options: \(options) with error: \(error)"
|
||||
// owsFailDebug(message)
|
||||
// }
|
||||
//
|
||||
// if audioSessionChanged {
|
||||
// Logger.info("")
|
||||
// self.delegate?.callAudioServiceDidChangeAudioSession(self)
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -1,133 +0,0 @@
|
|||
////
|
||||
//// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
////
|
||||
//
|
||||
//import UIKit
|
||||
//import CallKit
|
||||
//import SignalUtilitiesKit
|
||||
//
|
||||
///**
|
||||
// * Requests actions from CallKit
|
||||
// *
|
||||
// * @Discussion:
|
||||
// * Based on SpeakerboxCallManager, from the Apple CallKit Example app. Though, it's responsibilities are mostly
|
||||
// * mirrored (and delegated from) CallKitCallUIAdaptee.
|
||||
// * TODO: Would it simplify things to merge this into CallKitCallUIAdaptee?
|
||||
// */
|
||||
//@available(iOS 10.0, *)
|
||||
//final class CallKitCallManager: NSObject {
|
||||
//
|
||||
// let callController = CXCallController()
|
||||
// let showNamesOnCallScreen: Bool
|
||||
//
|
||||
// @objc
|
||||
// static let kAnonymousCallHandlePrefix = "Signal:"
|
||||
//
|
||||
// required init(showNamesOnCallScreen: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// self.showNamesOnCallScreen = showNamesOnCallScreen
|
||||
// super.init()
|
||||
//
|
||||
// // We cannot assert singleton here, because this class gets rebuilt when the user changes relevant call settings
|
||||
// }
|
||||
//
|
||||
// // MARK: Actions
|
||||
//
|
||||
// func startCall(_ call: SignalCall) {
|
||||
// var handle: CXHandle
|
||||
//
|
||||
// if showNamesOnCallScreen {
|
||||
// handle = CXHandle(type: .phoneNumber, value: call.remotePhoneNumber)
|
||||
// } else {
|
||||
// let callKitId = CallKitCallManager.kAnonymousCallHandlePrefix + call.localId.uuidString
|
||||
// handle = CXHandle(type: .generic, value: callKitId)
|
||||
// OWSPrimaryStorage.shared().setPhoneNumber(call.remotePhoneNumber, forCallKitId: callKitId)
|
||||
// }
|
||||
//
|
||||
// let startCallAction = CXStartCallAction(call: call.localId, handle: handle)
|
||||
//
|
||||
// startCallAction.isVideo = call.hasLocalVideo
|
||||
//
|
||||
// let transaction = CXTransaction()
|
||||
// transaction.addAction(startCallAction)
|
||||
//
|
||||
// requestTransaction(transaction)
|
||||
// }
|
||||
//
|
||||
// func localHangup(call: SignalCall) {
|
||||
// let endCallAction = CXEndCallAction(call: call.localId)
|
||||
// let transaction = CXTransaction()
|
||||
// transaction.addAction(endCallAction)
|
||||
//
|
||||
// requestTransaction(transaction)
|
||||
// }
|
||||
//
|
||||
// func setHeld(call: SignalCall, onHold: Bool) {
|
||||
// let setHeldCallAction = CXSetHeldCallAction(call: call.localId, onHold: onHold)
|
||||
// let transaction = CXTransaction()
|
||||
// transaction.addAction(setHeldCallAction)
|
||||
//
|
||||
// requestTransaction(transaction)
|
||||
// }
|
||||
//
|
||||
// func setIsMuted(call: SignalCall, isMuted: Bool) {
|
||||
// let muteCallAction = CXSetMutedCallAction(call: call.localId, muted: isMuted)
|
||||
// let transaction = CXTransaction()
|
||||
// transaction.addAction(muteCallAction)
|
||||
//
|
||||
// requestTransaction(transaction)
|
||||
// }
|
||||
//
|
||||
// func answer(call: SignalCall) {
|
||||
// let answerCallAction = CXAnswerCallAction(call: call.localId)
|
||||
// let transaction = CXTransaction()
|
||||
// transaction.addAction(answerCallAction)
|
||||
//
|
||||
// requestTransaction(transaction)
|
||||
// }
|
||||
//
|
||||
// private func requestTransaction(_ transaction: CXTransaction) {
|
||||
// callController.request(transaction) { error in
|
||||
// if let error = error {
|
||||
// Logger.error("Error requesting transaction: \(error)")
|
||||
// } else {
|
||||
// Logger.debug("Requested transaction successfully")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // MARK: Call Management
|
||||
//
|
||||
// private(set) var calls = [SignalCall]()
|
||||
//
|
||||
// func callWithLocalId(_ localId: UUID) -> SignalCall? {
|
||||
// guard let index = calls.firstIndex(where: { $0.localId == localId }) else {
|
||||
// return nil
|
||||
// }
|
||||
// return calls[index]
|
||||
// }
|
||||
//
|
||||
// func addCall(_ call: SignalCall) {
|
||||
// calls.append(call)
|
||||
// }
|
||||
//
|
||||
// func removeCall(_ call: SignalCall) {
|
||||
// calls.removeFirst(where: { $0 === call })
|
||||
// }
|
||||
//
|
||||
// func removeAllCalls() {
|
||||
// calls.removeAll()
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//fileprivate extension Array {
|
||||
//
|
||||
// mutating func removeFirst(where predicate: (Element) throws -> Bool) rethrows {
|
||||
// guard let index = try firstIndex(where: predicate) else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// remove(at: index)
|
||||
// }
|
||||
//}
|
|
@ -1,414 +0,0 @@
|
|||
////
|
||||
//// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
////
|
||||
//
|
||||
//import Foundation
|
||||
//import UIKit
|
||||
//import CallKit
|
||||
//import AVFoundation
|
||||
//import SignalUtilitiesKit
|
||||
//import SignalUtilitiesKit
|
||||
//
|
||||
///**
|
||||
// * Connects user interface to the CallService using CallKit.
|
||||
// *
|
||||
// * User interface is routed to the CallManager which requests CXCallActions, and if the CXProvider accepts them,
|
||||
// * their corresponding consequences are implmented in the CXProviderDelegate methods, e.g. using the CallService
|
||||
// */
|
||||
//@available(iOS 10.0, *)
|
||||
//final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate {
|
||||
//
|
||||
// private let callManager: CallKitCallManager
|
||||
// internal let callService: CallService
|
||||
// internal let notificationPresenter: NotificationPresenter
|
||||
// internal let contactsManager: OWSContactsManager
|
||||
// private let showNamesOnCallScreen: Bool
|
||||
// private let provider: CXProvider
|
||||
// private let audioActivity: AudioActivity
|
||||
//
|
||||
// // CallKit handles incoming ringer stop/start for us. Yay!
|
||||
// let hasManualRinger = false
|
||||
//
|
||||
// // Instantiating more than one CXProvider can cause us to miss call transactions, so
|
||||
// // we maintain the provider across Adaptees using a singleton pattern
|
||||
// private static var _sharedProvider: CXProvider?
|
||||
// class func sharedProvider(useSystemCallLog: Bool) -> CXProvider {
|
||||
// let configuration = buildProviderConfiguration(useSystemCallLog: useSystemCallLog)
|
||||
//
|
||||
// if let sharedProvider = self._sharedProvider {
|
||||
// sharedProvider.configuration = configuration
|
||||
// return sharedProvider
|
||||
// } else {
|
||||
// SwiftSingletons.register(self)
|
||||
// let provider = CXProvider(configuration: configuration)
|
||||
// _sharedProvider = provider
|
||||
// return provider
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // The app's provider configuration, representing its CallKit capabilities
|
||||
// class func buildProviderConfiguration(useSystemCallLog: Bool) -> CXProviderConfiguration {
|
||||
// let localizedName = NSLocalizedString("APPLICATION_NAME", comment: "Name of application")
|
||||
// let providerConfiguration = CXProviderConfiguration(localizedName: localizedName)
|
||||
//
|
||||
// providerConfiguration.supportsVideo = true
|
||||
//
|
||||
// providerConfiguration.maximumCallGroups = 1
|
||||
//
|
||||
// providerConfiguration.maximumCallsPerCallGroup = 1
|
||||
//
|
||||
// providerConfiguration.supportedHandleTypes = [.phoneNumber, .generic]
|
||||
//
|
||||
// let iconMaskImage = #imageLiteral(resourceName: "logoSignal")
|
||||
// providerConfiguration.iconTemplateImageData = iconMaskImage.pngData()
|
||||
//
|
||||
// // We don't set the ringtoneSound property, so that we use either the
|
||||
// // default iOS ringtone OR the custom ringtone associated with this user's
|
||||
// // system contact, if possible (iOS 11 or later).
|
||||
//
|
||||
// if #available(iOS 11.0, *) {
|
||||
// providerConfiguration.includesCallsInRecents = useSystemCallLog
|
||||
// } else {
|
||||
// // not configurable for iOS10+
|
||||
// assert(useSystemCallLog)
|
||||
// }
|
||||
//
|
||||
// return providerConfiguration
|
||||
// }
|
||||
//
|
||||
// init(callService: CallService, contactsManager: OWSContactsManager, notificationPresenter: NotificationPresenter, showNamesOnCallScreen: Bool, useSystemCallLog: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.debug("")
|
||||
//
|
||||
// self.callManager = CallKitCallManager(showNamesOnCallScreen: showNamesOnCallScreen)
|
||||
// self.callService = callService
|
||||
// self.contactsManager = contactsManager
|
||||
// self.notificationPresenter = notificationPresenter
|
||||
//
|
||||
// self.provider = type(of: self).sharedProvider(useSystemCallLog: useSystemCallLog)
|
||||
//
|
||||
// self.audioActivity = AudioActivity(audioDescription: "[CallKitCallUIAdaptee]", behavior: .call)
|
||||
// self.showNamesOnCallScreen = showNamesOnCallScreen
|
||||
//
|
||||
// super.init()
|
||||
//
|
||||
// // We cannot assert singleton here, because this class gets rebuilt when the user changes relevant call settings
|
||||
//
|
||||
// self.provider.setDelegate(self, queue: nil)
|
||||
// }
|
||||
//
|
||||
// // MARK: Dependencies
|
||||
//
|
||||
// var audioSession: OWSAudioSession {
|
||||
// return Environment.shared.audioSession
|
||||
// }
|
||||
//
|
||||
// // MARK: CallUIAdaptee
|
||||
//
|
||||
// func startOutgoingCall(handle: String) -> SignalCall {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// let call = SignalCall.outgoingCall(localId: UUID(), remotePhoneNumber: handle)
|
||||
//
|
||||
// // make sure we don't terminate audio session during call
|
||||
// _ = self.audioSession.startAudioActivity(call.audioActivity)
|
||||
//
|
||||
// // Add the new outgoing call to the app's list of calls.
|
||||
// // So we can find it in the provider delegate callbacks.
|
||||
// callManager.addCall(call)
|
||||
// callManager.startCall(call)
|
||||
//
|
||||
// return call
|
||||
// }
|
||||
//
|
||||
// // Called from CallService after call has ended to clean up any remaining CallKit call state.
|
||||
// func failCall(_ call: SignalCall, error: CallError) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// switch error {
|
||||
// case .timeout(description: _):
|
||||
// provider.reportCall(with: call.localId, endedAt: Date(), reason: CXCallEndedReason.unanswered)
|
||||
// default:
|
||||
// provider.reportCall(with: call.localId, endedAt: Date(), reason: CXCallEndedReason.failed)
|
||||
// }
|
||||
//
|
||||
// self.callManager.removeCall(call)
|
||||
// }
|
||||
//
|
||||
// func reportIncomingCall(_ call: SignalCall, callerName: String) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// // Construct a CXCallUpdate describing the incoming call, including the caller.
|
||||
// let update = CXCallUpdate()
|
||||
//
|
||||
// if showNamesOnCallScreen {
|
||||
// update.localizedCallerName = self.contactsManager.stringForConversationTitle(withPhoneIdentifier: call.remotePhoneNumber)
|
||||
// update.remoteHandle = CXHandle(type: .phoneNumber, value: call.remotePhoneNumber)
|
||||
// } else {
|
||||
// let callKitId = CallKitCallManager.kAnonymousCallHandlePrefix + call.localId.uuidString
|
||||
// update.remoteHandle = CXHandle(type: .generic, value: callKitId)
|
||||
// OWSPrimaryStorage.shared().setPhoneNumber(call.remotePhoneNumber, forCallKitId: callKitId)
|
||||
// update.localizedCallerName = NSLocalizedString("CALLKIT_ANONYMOUS_CONTACT_NAME", comment: "The generic name used for calls if CallKit privacy is enabled")
|
||||
// }
|
||||
//
|
||||
// update.hasVideo = call.hasLocalVideo
|
||||
//
|
||||
// disableUnsupportedFeatures(callUpdate: update)
|
||||
//
|
||||
// // Report the incoming call to the system
|
||||
// provider.reportNewIncomingCall(with: call.localId, update: update) { error in
|
||||
// /*
|
||||
// Only add incoming call to the app's list of calls if the call was allowed (i.e. there was no error)
|
||||
// since calls may be "denied" for various legitimate reasons. See CXErrorCodeIncomingCallError.
|
||||
// */
|
||||
// guard error == nil else {
|
||||
// Logger.error("failed to report new incoming call")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.callManager.addCall(call)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func answerCall(localId: UUID) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// owsFailDebug("CallKit should answer calls via system call screen, not via notifications.")
|
||||
// }
|
||||
//
|
||||
// func answerCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// callManager.answer(call: call)
|
||||
// }
|
||||
//
|
||||
// func declineCall(localId: UUID) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// owsFailDebug("CallKit should decline calls via system call screen, not via notifications.")
|
||||
// }
|
||||
//
|
||||
// func declineCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// callManager.localHangup(call: call)
|
||||
// }
|
||||
//
|
||||
// func recipientAcceptedCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// self.provider.reportOutgoingCall(with: call.localId, connectedAt: nil)
|
||||
//
|
||||
// let update = CXCallUpdate()
|
||||
// disableUnsupportedFeatures(callUpdate: update)
|
||||
//
|
||||
// provider.reportCall(with: call.localId, updated: update)
|
||||
// }
|
||||
//
|
||||
// func localHangupCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// callManager.localHangup(call: call)
|
||||
// }
|
||||
//
|
||||
// func remoteDidHangupCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// provider.reportCall(with: call.localId, endedAt: nil, reason: CXCallEndedReason.remoteEnded)
|
||||
// }
|
||||
//
|
||||
// func remoteBusy(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// provider.reportCall(with: call.localId, endedAt: nil, reason: CXCallEndedReason.unanswered)
|
||||
// }
|
||||
//
|
||||
// func setIsMuted(call: SignalCall, isMuted: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// callManager.setIsMuted(call: call, isMuted: isMuted)
|
||||
// }
|
||||
//
|
||||
// func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.debug("")
|
||||
//
|
||||
// let update = CXCallUpdate()
|
||||
// update.hasVideo = hasLocalVideo
|
||||
//
|
||||
// // Update the CallKit UI.
|
||||
// provider.reportCall(with: call.localId, updated: update)
|
||||
//
|
||||
// self.callService.setHasLocalVideo(hasLocalVideo: hasLocalVideo)
|
||||
// }
|
||||
//
|
||||
// // MARK: CXProviderDelegate
|
||||
//
|
||||
// func providerDidReset(_ provider: CXProvider) {
|
||||
// AssertIsOnMainThread()
|
||||
// Logger.info("")
|
||||
//
|
||||
// // End any ongoing calls if the provider resets, and remove them from the app's list of calls,
|
||||
// // since they are no longer valid.
|
||||
// callService.handleFailedCurrentCall(error: .providerReset)
|
||||
//
|
||||
// // Remove all calls from the app's list of calls.
|
||||
// callManager.removeAllCalls()
|
||||
// }
|
||||
//
|
||||
// func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.info("CXStartCallAction")
|
||||
//
|
||||
// guard let call = callManager.callWithLocalId(action.callUUID) else {
|
||||
// Logger.error("unable to find call")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // We can't wait for long before fulfilling the CXAction, else CallKit will show a "Failed Call". We don't
|
||||
// // actually need to wait for the outcome of the handleOutgoingCall promise, because it handles any errors by
|
||||
// // manually failing the call.
|
||||
// self.callService.handleOutgoingCall(call).retainUntilComplete()
|
||||
//
|
||||
// action.fulfill()
|
||||
// self.provider.reportOutgoingCall(with: call.localId, startedConnectingAt: nil)
|
||||
//
|
||||
// // Update the name used in the CallKit UI for outgoing calls when the user prefers not to show names
|
||||
// // in ther notifications
|
||||
// if !showNamesOnCallScreen {
|
||||
// let update = CXCallUpdate()
|
||||
// update.localizedCallerName = NSLocalizedString("CALLKIT_ANONYMOUS_CONTACT_NAME",
|
||||
// comment: "The generic name used for calls if CallKit privacy is enabled")
|
||||
// provider.reportCall(with: call.localId, updated: update)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.info("Received \(#function) CXAnswerCallAction")
|
||||
// // Retrieve the instance corresponding to the action's call UUID
|
||||
// guard let call = callManager.callWithLocalId(action.callUUID) else {
|
||||
// action.fail()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.callService.handleAnswerCall(call)
|
||||
// self.showCall(call)
|
||||
// action.fulfill()
|
||||
// }
|
||||
//
|
||||
// public func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.info("Received \(#function) CXEndCallAction")
|
||||
// guard let call = callManager.callWithLocalId(action.callUUID) else {
|
||||
// Logger.error("trying to end unknown call with localId: \(action.callUUID)")
|
||||
// action.fail()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.callService.handleLocalHungupCall(call)
|
||||
//
|
||||
// // Signal to the system that the action has been successfully performed.
|
||||
// action.fulfill()
|
||||
//
|
||||
// // Remove the ended call from the app's list of calls.
|
||||
// self.callManager.removeCall(call)
|
||||
// }
|
||||
//
|
||||
// public func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.info("Received \(#function) CXSetHeldCallAction")
|
||||
// guard let call = callManager.callWithLocalId(action.callUUID) else {
|
||||
// action.fail()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// // Update the SignalCall's underlying hold state.
|
||||
// self.callService.setIsOnHold(call: call, isOnHold: action.isOnHold)
|
||||
//
|
||||
// // Signal to the system that the action has been successfully performed.
|
||||
// action.fulfill()
|
||||
// }
|
||||
//
|
||||
// public func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.info("Received \(#function) CXSetMutedCallAction")
|
||||
// guard let call = callManager.callWithLocalId(action.callUUID) else {
|
||||
// Logger.error("Failing CXSetMutedCallAction for unknown call: \(action.callUUID)")
|
||||
// action.fail()
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// self.callService.setIsMuted(call: call, isMuted: action.isMuted)
|
||||
// action.fulfill()
|
||||
// }
|
||||
//
|
||||
// public func provider(_ provider: CXProvider, perform action: CXSetGroupCallAction) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.warn("unimplemented \(#function) for CXSetGroupCallAction")
|
||||
// }
|
||||
//
|
||||
// public func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.warn("unimplemented \(#function) for CXPlayDTMFCallAction")
|
||||
// }
|
||||
//
|
||||
// func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// owsFailDebug("Timed out while performing \(action)")
|
||||
//
|
||||
// // React to the action timeout if necessary, such as showing an error UI.
|
||||
// }
|
||||
//
|
||||
// func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.debug("Received")
|
||||
//
|
||||
// _ = self.audioSession.startAudioActivity(self.audioActivity)
|
||||
// self.audioSession.isRTCAudioEnabled = true
|
||||
// }
|
||||
//
|
||||
// func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// Logger.debug("Received")
|
||||
// self.audioSession.isRTCAudioEnabled = false
|
||||
// self.audioSession.endAudioActivity(self.audioActivity)
|
||||
// }
|
||||
//
|
||||
// // MARK: - Util
|
||||
//
|
||||
// private func disableUnsupportedFeatures(callUpdate: CXCallUpdate) {
|
||||
// // Call Holding is failing to restart audio when "swapping" calls on the CallKit screen
|
||||
// // until user returns to in-app call screen.
|
||||
// callUpdate.supportsHolding = false
|
||||
//
|
||||
// // Not yet supported
|
||||
// callUpdate.supportsGrouping = false
|
||||
// callUpdate.supportsUngrouping = false
|
||||
//
|
||||
// // Is there any reason to support this?
|
||||
// callUpdate.supportsDTMF = false
|
||||
// }
|
||||
//}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,307 +0,0 @@
|
|||
////
|
||||
//// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
////
|
||||
//
|
||||
//import Foundation
|
||||
//import PromiseKit
|
||||
//import CallKit
|
||||
//import SignalUtilitiesKit
|
||||
//import SignalUtilitiesKit
|
||||
//import WebRTC
|
||||
//
|
||||
//protocol CallUIAdaptee {
|
||||
// var notificationPresenter: NotificationPresenter { get }
|
||||
// var callService: CallService { get }
|
||||
// var hasManualRinger: Bool { get }
|
||||
//
|
||||
// func startOutgoingCall(handle: String) -> SignalCall
|
||||
// func reportIncomingCall(_ call: SignalCall, callerName: String)
|
||||
// func reportMissedCall(_ call: SignalCall, callerName: String)
|
||||
// func answerCall(localId: UUID)
|
||||
// func answerCall(_ call: SignalCall)
|
||||
// func declineCall(localId: UUID)
|
||||
// func declineCall(_ call: SignalCall)
|
||||
// func recipientAcceptedCall(_ call: SignalCall)
|
||||
// func localHangupCall(_ call: SignalCall)
|
||||
// func remoteDidHangupCall(_ call: SignalCall)
|
||||
// func remoteBusy(_ call: SignalCall)
|
||||
// func failCall(_ call: SignalCall, error: CallError)
|
||||
// func setIsMuted(call: SignalCall, isMuted: Bool)
|
||||
// func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool)
|
||||
// func startAndShowOutgoingCall(recipientId: String, hasLocalVideo: Bool)
|
||||
//}
|
||||
//
|
||||
//// Shared default implementations
|
||||
//extension CallUIAdaptee {
|
||||
// internal func showCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// let callViewController = CallViewController(call: call)
|
||||
// callViewController.modalTransitionStyle = .crossDissolve
|
||||
//
|
||||
// if CallViewController.kShowCallViewOnSeparateWindow {
|
||||
// OWSWindowManager.shared().startCall(callViewController)
|
||||
// } else {
|
||||
// guard let presentingViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts else {
|
||||
// owsFailDebug("view controller unexpectedly nil")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if let presentedViewController = presentingViewController.presentedViewController {
|
||||
// presentedViewController.dismiss(animated: false) {
|
||||
// presentingViewController.present(callViewController, animated: true)
|
||||
// }
|
||||
// } else {
|
||||
// presentingViewController.present(callViewController, animated: true)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// internal func reportMissedCall(_ call: SignalCall, callerName: String) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// notificationPresenter.presentMissedCall(call, callerName: callerName)
|
||||
// }
|
||||
//
|
||||
// internal func startAndShowOutgoingCall(recipientId: String, hasLocalVideo: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// guard self.callService.call == nil else {
|
||||
// owsFailDebug("unexpectedly found an existing call when trying to start outgoing call: \(recipientId)")
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// let call = self.startOutgoingCall(handle: recipientId)
|
||||
// call.hasLocalVideo = hasLocalVideo
|
||||
// self.showCall(call)
|
||||
// }
|
||||
//}
|
||||
//
|
||||
///**
|
||||
// * Notify the user of call related activities.
|
||||
// * Driven by either a CallKit or System notifications adaptee
|
||||
// */
|
||||
//@objc public class CallUIAdapter: NSObject, CallServiceObserver {
|
||||
//
|
||||
// private let adaptee: CallUIAdaptee
|
||||
// private let contactsManager: OWSContactsManager
|
||||
// internal let audioService: CallAudioService
|
||||
// internal let callService: CallService
|
||||
//
|
||||
// public required init(callService: CallService, contactsManager: OWSContactsManager, notificationPresenter: NotificationPresenter) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// self.contactsManager = contactsManager
|
||||
// self.callService = callService
|
||||
//
|
||||
// if Platform.isSimulator {
|
||||
// // CallKit doesn't seem entirely supported in simulator.
|
||||
// // e.g. you can't receive calls in the call screen.
|
||||
// // So we use the non-CallKit call UI.
|
||||
// Logger.info("choosing non-callkit adaptee for simulator.")
|
||||
// adaptee = NonCallKitCallUIAdaptee(callService: callService, notificationPresenter: notificationPresenter)
|
||||
// } else if CallUIAdapter.isCallkitDisabledForLocale {
|
||||
// Logger.info("choosing non-callkit adaptee due to locale.")
|
||||
// adaptee = NonCallKitCallUIAdaptee(callService: callService, notificationPresenter: notificationPresenter)
|
||||
// } else if #available(iOS 11, *) {
|
||||
// Logger.info("choosing callkit adaptee for iOS11+")
|
||||
// let showNames = Environment.shared.preferences.notificationPreviewType() != .noNameNoPreview
|
||||
// let useSystemCallLog = Environment.shared.preferences.isSystemCallLogEnabled()
|
||||
//
|
||||
// adaptee = CallKitCallUIAdaptee(callService: callService, contactsManager: contactsManager, notificationPresenter: notificationPresenter, showNamesOnCallScreen: showNames, useSystemCallLog: useSystemCallLog)
|
||||
// } else if #available(iOS 10.0, *), Environment.shared.preferences.isCallKitEnabled() {
|
||||
// Logger.info("choosing callkit adaptee for iOS10")
|
||||
// let hideNames = Environment.shared.preferences.isCallKitPrivacyEnabled() || Environment.shared.preferences.notificationPreviewType() == .noNameNoPreview
|
||||
// let showNames = !hideNames
|
||||
//
|
||||
// // All CallKit calls use the system call log on iOS10
|
||||
// let useSystemCallLog = true
|
||||
//
|
||||
// adaptee = CallKitCallUIAdaptee(callService: callService, contactsManager: contactsManager, notificationPresenter: notificationPresenter, showNamesOnCallScreen: showNames, useSystemCallLog: useSystemCallLog)
|
||||
// } else {
|
||||
// Logger.info("choosing non-callkit adaptee")
|
||||
// adaptee = NonCallKitCallUIAdaptee(callService: callService, notificationPresenter: notificationPresenter)
|
||||
// }
|
||||
//
|
||||
// audioService = CallAudioService(handleRinging: adaptee.hasManualRinger)
|
||||
//
|
||||
// super.init()
|
||||
//
|
||||
// // We cannot assert singleton here, because this class gets rebuilt when the user changes relevant call settings
|
||||
//
|
||||
// callService.addObserverAndSyncState(observer: self)
|
||||
// }
|
||||
//
|
||||
// @objc
|
||||
// public static var isCallkitDisabledForLocale: Bool {
|
||||
// let locale = Locale.current
|
||||
// guard let regionCode = locale.regionCode else {
|
||||
// owsFailDebug("Missing region code.")
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// // Apple has stopped approving apps that use CallKit functionality in mainland China.
|
||||
// // When the "CN" region is enabled, this check simply switches to the same pre-CallKit
|
||||
// // interface that is still used by everyone on iOS 9.
|
||||
// //
|
||||
// // For further reference: https://forums.developer.apple.com/thread/103083
|
||||
// return regionCode == "CN"
|
||||
// }
|
||||
//
|
||||
// // MARK: Dependencies
|
||||
//
|
||||
// var audioSession: OWSAudioSession {
|
||||
// return Environment.shared.audioSession
|
||||
// }
|
||||
//
|
||||
// // MARK:
|
||||
//
|
||||
// internal func reportIncomingCall(_ call: SignalCall, thread: TSContactThread) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// // make sure we don't terminate audio session during call
|
||||
// _ = audioSession.startAudioActivity(call.audioActivity)
|
||||
//
|
||||
// let callerName = self.contactsManager.displayName(forPhoneIdentifier: call.remotePhoneNumber)
|
||||
// adaptee.reportIncomingCall(call, callerName: callerName)
|
||||
// }
|
||||
//
|
||||
// internal func reportMissedCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// let callerName = self.contactsManager.displayName(forPhoneIdentifier: call.remotePhoneNumber)
|
||||
// adaptee.reportMissedCall(call, callerName: callerName)
|
||||
// }
|
||||
//
|
||||
// internal func startOutgoingCall(handle: String) -> SignalCall {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// let call = adaptee.startOutgoingCall(handle: handle)
|
||||
// return call
|
||||
// }
|
||||
//
|
||||
// @objc public func answerCall(localId: UUID) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.answerCall(localId: localId)
|
||||
// }
|
||||
//
|
||||
// internal func answerCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.answerCall(call)
|
||||
// }
|
||||
//
|
||||
// @objc public func declineCall(localId: UUID) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.declineCall(localId: localId)
|
||||
// }
|
||||
//
|
||||
// internal func declineCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.declineCall(call)
|
||||
// }
|
||||
//
|
||||
// internal func didTerminateCall(_ call: SignalCall?) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// if let call = call {
|
||||
// self.audioSession.endAudioActivity(call.audioActivity)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @objc public func startAndShowOutgoingCall(recipientId: String, hasLocalVideo: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.startAndShowOutgoingCall(recipientId: recipientId, hasLocalVideo: hasLocalVideo)
|
||||
// }
|
||||
//
|
||||
// internal func recipientAcceptedCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.recipientAcceptedCall(call)
|
||||
// }
|
||||
//
|
||||
// internal func remoteDidHangupCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.remoteDidHangupCall(call)
|
||||
// }
|
||||
//
|
||||
// internal func remoteBusy(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.remoteBusy(call)
|
||||
// }
|
||||
//
|
||||
// internal func localHangupCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.localHangupCall(call)
|
||||
// }
|
||||
//
|
||||
// internal func failCall(_ call: SignalCall, error: CallError) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.failCall(call, error: error)
|
||||
// }
|
||||
//
|
||||
// internal func showCall(_ call: SignalCall) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.showCall(call)
|
||||
// }
|
||||
//
|
||||
// internal func setIsMuted(call: SignalCall, isMuted: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// // With CallKit, muting is handled by a CXAction, so it must go through the adaptee
|
||||
// adaptee.setIsMuted(call: call, isMuted: isMuted)
|
||||
// }
|
||||
//
|
||||
// internal func setHasLocalVideo(call: SignalCall, hasLocalVideo: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// adaptee.setHasLocalVideo(call: call, hasLocalVideo: hasLocalVideo)
|
||||
// }
|
||||
//
|
||||
// internal func setAudioSource(call: SignalCall, audioSource: AudioSource?) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// // AudioSource is not handled by CallKit (e.g. there is no CXAction), so we handle it w/o going through the
|
||||
// // adaptee, relying on the AudioService CallObserver to put the system in a state consistent with the call's
|
||||
// // assigned property.
|
||||
// call.audioSource = audioSource
|
||||
// }
|
||||
//
|
||||
// internal func setCameraSource(call: SignalCall, isUsingFrontCamera: Bool) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// callService.setCameraSource(call: call, isUsingFrontCamera: isUsingFrontCamera)
|
||||
// }
|
||||
//
|
||||
// // CallKit handles ringing state on it's own. But for non-call kit we trigger ringing start/stop manually.
|
||||
// internal var hasManualRinger: Bool {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// return adaptee.hasManualRinger
|
||||
// }
|
||||
//
|
||||
// // MARK: - CallServiceObserver
|
||||
//
|
||||
// internal func didUpdateCall(call: SignalCall?) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// call?.addObserverAndSyncState(observer: audioService)
|
||||
// }
|
||||
//
|
||||
// internal func didUpdateVideoTracks(call: SignalCall?,
|
||||
// localCaptureSession: AVCaptureSession?,
|
||||
// remoteVideoTrack: RTCVideoTrack?) {
|
||||
// AssertIsOnMainThread()
|
||||
//
|
||||
// audioService.didUpdateVideoTracks(call: call)
|
||||
// }
|
||||
//}
|
|
@ -1,83 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol CallVideoHintViewDelegate: AnyObject {
|
||||
func didTapCallVideoHintView(_ videoHintView: CallVideoHintView)
|
||||
}
|
||||
|
||||
class CallVideoHintView: UIView {
|
||||
let label = UILabel()
|
||||
var tapGesture: UITapGestureRecognizer!
|
||||
weak var delegate: CallVideoHintViewDelegate?
|
||||
|
||||
let kTailHMargin: CGFloat = 12
|
||||
let kTailWidth: CGFloat = 16
|
||||
let kTailHeight: CGFloat = 8
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap(tapGesture:)))
|
||||
addGestureRecognizer(tapGesture)
|
||||
|
||||
let layerView = OWSLayerView(frame: .zero) { _ in }
|
||||
let shapeLayer = CAShapeLayer()
|
||||
shapeLayer.fillColor = UIColor.ows_signalBlue.cgColor
|
||||
layerView.layer.addSublayer(shapeLayer)
|
||||
addSubview(layerView)
|
||||
layerView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
let container = UIView()
|
||||
addSubview(container)
|
||||
container.autoSetDimension(.width, toSize: ScaleFromIPhone5(250), relation: .lessThanOrEqual)
|
||||
container.layoutMargins = UIEdgeInsets(top: 7, leading: 12, bottom: 7, trailing: 12)
|
||||
container.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: 0, leading: 0, bottom: kTailHeight, trailing: 0))
|
||||
|
||||
container.addSubview(label)
|
||||
label.autoPinEdgesToSuperviewMargins()
|
||||
label.setCompressionResistanceHigh()
|
||||
label.setContentHuggingHigh()
|
||||
label.font = UIFont.ows_dynamicTypeBody
|
||||
label.textColor = .ows_white
|
||||
label.numberOfLines = 0
|
||||
label.text = NSLocalizedString("CALL_VIEW_ENABLE_VIDEO_HINT", comment: "tooltip label when remote party has enabled their video")
|
||||
|
||||
layerView.layoutCallback = { view in
|
||||
let bezierPath = UIBezierPath()
|
||||
|
||||
// Bubble
|
||||
let bubbleBounds = container.bounds
|
||||
bezierPath.append(UIBezierPath(roundedRect: bubbleBounds, cornerRadius: 8))
|
||||
|
||||
// Tail
|
||||
var tailBottom = CGPoint(x: self.kTailHMargin + self.kTailWidth * 0.5, y: view.height())
|
||||
var tailLeft = CGPoint(x: self.kTailHMargin, y: view.height() - self.kTailHeight)
|
||||
var tailRight = CGPoint(x: self.kTailHMargin + self.kTailWidth, y: view.height() - self.kTailHeight)
|
||||
if (!CurrentAppContext().isRTL) {
|
||||
tailBottom.x = view.width() - tailBottom.x
|
||||
tailLeft.x = view.width() - tailLeft.x
|
||||
tailRight.x = view.width() - tailRight.x
|
||||
}
|
||||
bezierPath.move(to: tailBottom)
|
||||
bezierPath.addLine(to: tailLeft)
|
||||
bezierPath.addLine(to: tailRight)
|
||||
bezierPath.addLine(to: tailBottom)
|
||||
shapeLayer.path = bezierPath.cgPath
|
||||
shapeLayer.frame = view.bounds
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@objc
|
||||
func didTap(tapGesture: UITapGestureRecognizer) {
|
||||
self.delegate?.didTapCallVideoHintView(self)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,532 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc
|
||||
class OWSColorPickerAccessoryView: NeverClearView {
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return CGSize(width: kSwatchSize, height: kSwatchSize)
|
||||
}
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
return self.intrinsicContentSize
|
||||
}
|
||||
|
||||
let kSwatchSize: CGFloat = 24
|
||||
|
||||
@objc
|
||||
required init(color: UIColor) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
let circleView = CircleView()
|
||||
circleView.backgroundColor = color
|
||||
addSubview(circleView)
|
||||
circleView.autoSetDimensions(to: CGSize(width: kSwatchSize, height: kSwatchSize))
|
||||
circleView.autoPinEdgesToSuperviewEdges()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@objc (OWSCircleView)
|
||||
class CircleView: UIView {
|
||||
override var bounds: CGRect {
|
||||
didSet {
|
||||
self.layer.cornerRadius = self.bounds.size.height / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol ColorViewDelegate: class {
|
||||
func colorViewWasTapped(_ colorView: ColorView)
|
||||
}
|
||||
|
||||
class ColorView: UIView {
|
||||
public weak var delegate: ColorViewDelegate?
|
||||
public let conversationColor: OWSConversationColor
|
||||
|
||||
private let swatchView: CircleView
|
||||
private let selectedRing: CircleView
|
||||
public var isSelected: Bool = false {
|
||||
didSet {
|
||||
self.selectedRing.isHidden = !isSelected
|
||||
}
|
||||
}
|
||||
|
||||
required init(conversationColor: OWSConversationColor) {
|
||||
self.conversationColor = conversationColor
|
||||
self.swatchView = CircleView()
|
||||
self.selectedRing = CircleView()
|
||||
|
||||
super.init(frame: .zero)
|
||||
self.addSubview(selectedRing)
|
||||
self.addSubview(swatchView)
|
||||
|
||||
// Selected Ring
|
||||
let cellHeight: CGFloat = ScaleFromIPhone5(60)
|
||||
selectedRing.autoSetDimensions(to: CGSize(width: cellHeight, height: cellHeight))
|
||||
|
||||
selectedRing.layer.borderColor = Theme.secondaryColor.cgColor
|
||||
selectedRing.layer.borderWidth = 2
|
||||
selectedRing.autoPinEdgesToSuperviewEdges()
|
||||
selectedRing.isHidden = true
|
||||
|
||||
// Color Swatch
|
||||
swatchView.backgroundColor = conversationColor.primaryColor
|
||||
|
||||
let swatchSize: CGFloat = ScaleFromIPhone5(46)
|
||||
swatchView.autoSetDimensions(to: CGSize(width: swatchSize, height: swatchSize))
|
||||
|
||||
swatchView.autoCenterInSuperview()
|
||||
|
||||
// gestures
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
|
||||
self.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@objc
|
||||
func didTap() {
|
||||
delegate?.colorViewWasTapped(self)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
protocol ColorPickerDelegate: class {
|
||||
func colorPicker(_ colorPicker: ColorPicker, didPickConversationColor conversationColor: OWSConversationColor)
|
||||
}
|
||||
|
||||
@objc(OWSColorPicker)
|
||||
class ColorPicker: NSObject, ColorPickerViewDelegate {
|
||||
|
||||
@objc
|
||||
public weak var delegate: ColorPickerDelegate?
|
||||
|
||||
@objc
|
||||
let sheetViewController: SheetViewController
|
||||
|
||||
@objc
|
||||
init(thread: TSThread) {
|
||||
let colorName = thread.conversationColorName
|
||||
let currentConversationColor = OWSConversationColor.conversationColorOrDefault(colorName: colorName)
|
||||
sheetViewController = SheetViewController()
|
||||
|
||||
super.init()
|
||||
|
||||
let colorPickerView = ColorPickerView(thread: thread)
|
||||
colorPickerView.delegate = self
|
||||
colorPickerView.select(conversationColor: currentConversationColor)
|
||||
|
||||
sheetViewController.contentView.addSubview(colorPickerView)
|
||||
colorPickerView.autoPinEdgesToSuperviewEdges()
|
||||
}
|
||||
|
||||
// MARK: ColorPickerViewDelegate
|
||||
|
||||
func colorPickerView(_ colorPickerView: ColorPickerView, didPickConversationColor conversationColor: OWSConversationColor) {
|
||||
self.delegate?.colorPicker(self, didPickConversationColor: conversationColor)
|
||||
}
|
||||
}
|
||||
|
||||
protocol ColorPickerViewDelegate: class {
|
||||
func colorPickerView(_ colorPickerView: ColorPickerView, didPickConversationColor conversationColor: OWSConversationColor)
|
||||
}
|
||||
|
||||
class ColorPickerView: UIView, ColorViewDelegate {
|
||||
|
||||
private let colorViews: [ColorView]
|
||||
let conversationStyle: ConversationStyle
|
||||
var outgoingMessageView = OWSMessageBubbleView(forAutoLayout: ())
|
||||
var incomingMessageView = OWSMessageBubbleView(forAutoLayout: ())
|
||||
weak var delegate: ColorPickerViewDelegate?
|
||||
|
||||
// This is mostly a developer convenience - OWSMessageCell asserts at some point
|
||||
// that the available method width is greater than 0.
|
||||
// We ultimately use the width of the picker view which will be larger.
|
||||
let kMinimumConversationWidth: CGFloat = 300
|
||||
override var bounds: CGRect {
|
||||
didSet {
|
||||
updateMockConversationView()
|
||||
}
|
||||
}
|
||||
|
||||
let mockConversationView: UIView = UIView()
|
||||
|
||||
init(thread: TSThread) {
|
||||
let allConversationColors = OWSConversationColor.conversationColorNames.map { OWSConversationColor.conversationColorOrDefault(colorName: $0) }
|
||||
|
||||
self.colorViews = allConversationColors.map { ColorView(conversationColor: $0) }
|
||||
|
||||
self.conversationStyle = ConversationStyle(thread: thread)
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
colorViews.forEach { $0.delegate = self }
|
||||
|
||||
let headerView = self.buildHeaderView()
|
||||
mockConversationView.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
|
||||
mockConversationView.backgroundColor = Theme.backgroundColor
|
||||
self.updateMockConversationView()
|
||||
|
||||
let paletteView = self.buildPaletteView(colorViews: colorViews)
|
||||
|
||||
let rowsStackView = UIStackView(arrangedSubviews: [headerView, mockConversationView, paletteView])
|
||||
rowsStackView.axis = .vertical
|
||||
addSubview(rowsStackView)
|
||||
rowsStackView.autoPinEdgesToSuperviewEdges()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
// MARK: ColorViewDelegate
|
||||
|
||||
func colorViewWasTapped(_ colorView: ColorView) {
|
||||
self.select(conversationColor: colorView.conversationColor)
|
||||
self.delegate?.colorPickerView(self, didPickConversationColor: colorView.conversationColor)
|
||||
updateMockConversationView()
|
||||
}
|
||||
|
||||
fileprivate func select(conversationColor selectedConversationColor: OWSConversationColor) {
|
||||
colorViews.forEach { colorView in
|
||||
colorView.isSelected = colorView.conversationColor == selectedConversationColor
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: View Building
|
||||
|
||||
private func buildHeaderView() -> UIView {
|
||||
let headerView = UIView()
|
||||
headerView.layoutMargins = UIEdgeInsets(top: 15, left: 16, bottom: 15, right: 16)
|
||||
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = NSLocalizedString("COLOR_PICKER_SHEET_TITLE", comment: "Modal Sheet title when picking a conversation color.")
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.font = UIFont.ows_dynamicTypeBody.ows_mediumWeight()
|
||||
titleLabel.textColor = Theme.primaryColor
|
||||
|
||||
headerView.addSubview(titleLabel)
|
||||
titleLabel.ows_autoPinToSuperviewMargins()
|
||||
|
||||
let bottomBorderView = UIView()
|
||||
bottomBorderView.backgroundColor = Theme.hairlineColor
|
||||
headerView.addSubview(bottomBorderView)
|
||||
bottomBorderView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top)
|
||||
bottomBorderView.autoSetDimension(.height, toSize: CGHairlineWidth())
|
||||
|
||||
return headerView
|
||||
}
|
||||
|
||||
private func updateMockConversationView() {
|
||||
/*
|
||||
conversationStyle.viewWidth = max(bounds.size.width, kMinimumConversationWidth)
|
||||
mockConversationView.subviews.forEach { $0.removeFromSuperview() }
|
||||
|
||||
// outgoing
|
||||
outgoingMessageView = OWSMessageBubbleView(forAutoLayout: ())
|
||||
let outgoingItem = MockConversationViewItem()
|
||||
let outgoingText = NSLocalizedString("COLOR_PICKER_DEMO_MESSAGE_1", comment: "The first of two messages demonstrating the chosen conversation color, by rendering this message in an outgoing message bubble.")
|
||||
outgoingItem.interaction = MockOutgoingMessage(messageBody: outgoingText)
|
||||
outgoingItem.displayableBodyText = DisplayableText.displayableText(outgoingText)
|
||||
outgoingItem.interactionType = .outgoingMessage
|
||||
|
||||
outgoingMessageView.viewItem = outgoingItem
|
||||
outgoingMessageView.cellMediaCache = NSCache()
|
||||
outgoingMessageView.conversationStyle = conversationStyle
|
||||
outgoingMessageView.configureViews()
|
||||
outgoingMessageView.loadContent()
|
||||
let outgoingCell = UIView()
|
||||
outgoingCell.addSubview(outgoingMessageView)
|
||||
outgoingMessageView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .leading)
|
||||
let outgoingSize = outgoingMessageView.measureSize()
|
||||
outgoingMessageView.autoSetDimensions(to: outgoingSize)
|
||||
|
||||
// incoming
|
||||
incomingMessageView = OWSMessageBubbleView(forAutoLayout: ())
|
||||
let incomingItem = MockConversationViewItem()
|
||||
let incomingText = NSLocalizedString("COLOR_PICKER_DEMO_MESSAGE_2", comment: "The second of two messages demonstrating the chosen conversation color, by rendering this message in an incoming message bubble.")
|
||||
incomingItem.interaction = MockIncomingMessage(messageBody: incomingText)
|
||||
incomingItem.displayableBodyText = DisplayableText.displayableText(incomingText)
|
||||
incomingItem.interactionType = .incomingMessage
|
||||
|
||||
incomingMessageView.viewItem = incomingItem
|
||||
incomingMessageView.cellMediaCache = NSCache()
|
||||
incomingMessageView.conversationStyle = conversationStyle
|
||||
incomingMessageView.configureViews()
|
||||
incomingMessageView.loadContent()
|
||||
let incomingCell = UIView()
|
||||
incomingCell.addSubview(incomingMessageView)
|
||||
incomingMessageView.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .trailing)
|
||||
let incomingSize = incomingMessageView.measureSize()
|
||||
incomingMessageView.autoSetDimensions(to: incomingSize)
|
||||
|
||||
let messagesStackView = UIStackView(arrangedSubviews: [outgoingCell, incomingCell])
|
||||
messagesStackView.axis = .vertical
|
||||
messagesStackView.spacing = 12
|
||||
|
||||
mockConversationView.addSubview(messagesStackView)
|
||||
messagesStackView.autoPinEdgesToSuperviewMargins()
|
||||
*/
|
||||
}
|
||||
|
||||
private func buildPaletteView(colorViews: [ColorView]) -> UIView {
|
||||
let paletteView = UIView()
|
||||
let paletteMargin = ScaleFromIPhone5(12)
|
||||
paletteView.layoutMargins = UIEdgeInsets(top: paletteMargin, left: paletteMargin, bottom: 0, right: paletteMargin)
|
||||
|
||||
let kRowLength = 4
|
||||
let rows: [UIView] = colorViews.chunked(by: kRowLength).map { colorViewsInRow in
|
||||
let row = UIStackView(arrangedSubviews: colorViewsInRow)
|
||||
row.distribution = UIStackView.Distribution.equalSpacing
|
||||
return row
|
||||
}
|
||||
let rowsStackView = UIStackView(arrangedSubviews: rows)
|
||||
rowsStackView.axis = .vertical
|
||||
rowsStackView.spacing = ScaleFromIPhone5To7Plus(12, 30)
|
||||
|
||||
paletteView.addSubview(rowsStackView)
|
||||
rowsStackView.ows_autoPinToSuperviewMargins()
|
||||
|
||||
// no-op gesture to keep taps from dismissing SheetView
|
||||
paletteView.addGestureRecognizer(UITapGestureRecognizer(target: nil, action: nil))
|
||||
return paletteView
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Mock Classes for rendering demo conversation
|
||||
|
||||
/*
|
||||
@objc
|
||||
private class MockConversationViewItem: NSObject, ConversationViewItem {
|
||||
var userCanDeleteGroupMessage: Bool = false
|
||||
var isRSSFeed: Bool = false
|
||||
var interaction: TSInteraction = TSMessage()
|
||||
var interactionType: OWSInteractionType = OWSInteractionType.unknown
|
||||
var quotedReply: OWSQuotedReplyModel?
|
||||
var isGroupThread: Bool = false
|
||||
var hasBodyText: Bool = true
|
||||
var isQuotedReply: Bool = false
|
||||
var hasQuotedAttachment: Bool = false
|
||||
var hasQuotedText: Bool = false
|
||||
var hasCellHeader: Bool = false
|
||||
var isExpiringMessage: Bool = false
|
||||
var shouldShowDate: Bool = false
|
||||
var shouldShowSenderAvatar: Bool = false
|
||||
var senderName: NSAttributedString?
|
||||
var shouldHideFooter: Bool = false
|
||||
var isFirstInCluster: Bool = true
|
||||
var isLastInCluster: Bool = true
|
||||
var unreadIndicator: OWSUnreadIndicator?
|
||||
var lastAudioMessageView: OWSAudioMessageView?
|
||||
var audioDurationSeconds: CGFloat = 0
|
||||
var audioProgressSeconds: CGFloat = 0
|
||||
var messageCellType: OWSMessageCellType = .textOnlyMessage
|
||||
var displayableBodyText: DisplayableText?
|
||||
var attachmentStream: TSAttachmentStream?
|
||||
var attachmentPointer: TSAttachmentPointer?
|
||||
var mediaSize: CGSize = .zero
|
||||
var displayableQuotedText: DisplayableText?
|
||||
var quotedAttachmentMimetype: String?
|
||||
var quotedRecipientId: String?
|
||||
var didCellMediaFailToLoad: Bool = false
|
||||
var contactShare: ContactShareViewModel?
|
||||
var systemMessageText: String?
|
||||
var authorConversationColorName: String?
|
||||
var hasBodyTextActionContent: Bool = false
|
||||
var hasMediaActionContent: Bool = false
|
||||
var mediaAlbumItems: [ConversationMediaAlbumItem]?
|
||||
var hasCachedLayoutState: Bool = false
|
||||
var linkPreview: OWSLinkPreview?
|
||||
var linkPreviewAttachment: TSAttachment?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
func itemId() -> String {
|
||||
return interaction.uniqueId!
|
||||
}
|
||||
|
||||
func dequeueCell(for collectionView: UICollectionView, indexPath: IndexPath) -> ConversationViewCell {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return ConversationViewCell(forAutoLayout: ())
|
||||
}
|
||||
|
||||
func replace(_ interaction: TSInteraction, transaction: YapDatabaseReadTransaction) {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func clearCachedLayoutState() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func copyMediaAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func copyTextAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func shareMediaAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func shareTextAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func saveMediaAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func deleteAction() {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func canCopyMedia() -> Bool {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return false
|
||||
}
|
||||
|
||||
func canSaveMedia() -> Bool {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return false
|
||||
}
|
||||
|
||||
func audioPlaybackState() -> AudioPlaybackState {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return AudioPlaybackState.paused
|
||||
}
|
||||
|
||||
func setAudioPlaybackState(_ state: AudioPlaybackState) {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func setAudioProgress(_ progress: CGFloat, duration: CGFloat) {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return
|
||||
}
|
||||
|
||||
func cellSize() -> CGSize {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return CGSize.zero
|
||||
}
|
||||
|
||||
func vSpacing(withPreviousLayoutItem previousLayoutItem: ConversationViewLayoutItem) -> CGFloat {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return 2
|
||||
}
|
||||
|
||||
func firstValidAlbumAttachment() -> TSAttachmentStream? {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return nil
|
||||
}
|
||||
|
||||
func mediaAlbumHasFailedAttachment() -> Bool {
|
||||
owsFailDebug("unexpected invocation")
|
||||
return false
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
private class MockIncomingMessage: TSIncomingMessage {
|
||||
init(messageBody: String) {
|
||||
super.init(incomingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(),
|
||||
in: TSThread(),
|
||||
authorId: "+fake-id",
|
||||
sourceDeviceId: 1,
|
||||
messageBody: messageBody,
|
||||
attachmentIds: [],
|
||||
expiresInSeconds: 0,
|
||||
quotedMessage: nil,
|
||||
contactShare: nil,
|
||||
linkPreview: nil,
|
||||
serverTimestamp: nil,
|
||||
wasReceivedByUD: false)
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
required init(dictionary dictionaryValue: [String: Any]!) throws {
|
||||
fatalError("init(dictionary:) has not been implemented")
|
||||
}
|
||||
|
||||
override func save(with transaction: YapDatabaseReadWriteTransaction) {
|
||||
// no - op
|
||||
owsFailDebug("shouldn't save mock message")
|
||||
}
|
||||
}
|
||||
|
||||
private class MockOutgoingMessage: TSOutgoingMessage {
|
||||
init(messageBody: String) {
|
||||
super.init(outgoingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(),
|
||||
in: nil,
|
||||
messageBody: messageBody,
|
||||
attachmentIds: [],
|
||||
expiresInSeconds: 0,
|
||||
expireStartedAt: 0,
|
||||
isVoiceMessage: false,
|
||||
groupMetaMessage: .unspecified,
|
||||
quotedMessage: nil,
|
||||
contactShare: nil,
|
||||
linkPreview: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
required init(dictionary dictionaryValue: [String: Any]!) throws {
|
||||
fatalError("init(dictionary:) has not been implemented")
|
||||
}
|
||||
|
||||
override func save(with transaction: YapDatabaseReadWriteTransaction) {
|
||||
// no - op
|
||||
owsFailDebug("shouldn't save mock message")
|
||||
}
|
||||
|
||||
class MockOutgoingMessageRecipientState: TSOutgoingMessageRecipientState {
|
||||
override var state: OWSOutgoingMessageRecipientState {
|
||||
return OWSOutgoingMessageRecipientState.sent
|
||||
}
|
||||
|
||||
override var deliveryTimestamp: NSNumber? {
|
||||
return NSNumber(value: NSDate.ows_millisecondTimeStamp())
|
||||
}
|
||||
|
||||
override var readTimestamp: NSNumber? {
|
||||
return NSNumber(value: NSDate.ows_millisecondTimeStamp())
|
||||
}
|
||||
}
|
||||
|
||||
override func readRecipientIds() -> [String] {
|
||||
// makes message appear as read
|
||||
return ["fake-non-empty-id"]
|
||||
}
|
||||
|
||||
override func recipientState(forRecipientId recipientId: String) -> TSOutgoingMessageRecipientState? {
|
||||
return MockOutgoingMessageRecipientState()
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalUtilitiesKit
|
||||
|
||||
let CompareSafetyNumbersActivityType = "org.whispersystems.signal.activity.CompareSafetyNumbers"
|
||||
|
||||
@objc(OWSCompareSafetyNumbersActivityDelegate)
|
||||
protocol CompareSafetyNumbersActivityDelegate {
|
||||
func compareSafetyNumbersActivitySucceeded(activity: CompareSafetyNumbersActivity)
|
||||
func compareSafetyNumbersActivity(_ activity: CompareSafetyNumbersActivity, failedWithError error: Error)
|
||||
}
|
||||
|
||||
@objc (OWSCompareSafetyNumbersActivity)
|
||||
class CompareSafetyNumbersActivity: UIActivity {
|
||||
|
||||
var mySafetyNumbers: String?
|
||||
let delegate: CompareSafetyNumbersActivityDelegate
|
||||
|
||||
@objc
|
||||
required init(delegate: CompareSafetyNumbersActivityDelegate) {
|
||||
self.delegate = delegate
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: UIActivity
|
||||
|
||||
override class var activityCategory: UIActivity.Category {
|
||||
get { return .action }
|
||||
}
|
||||
|
||||
override var activityType: UIActivity.ActivityType? {
|
||||
get { return UIActivity.ActivityType(rawValue: CompareSafetyNumbersActivityType) }
|
||||
}
|
||||
|
||||
override var activityTitle: String? {
|
||||
get {
|
||||
return NSLocalizedString("COMPARE_SAFETY_NUMBER_ACTION", comment: "Activity Sheet label")
|
||||
}
|
||||
}
|
||||
|
||||
override var activityImage: UIImage? {
|
||||
get {
|
||||
return #imageLiteral(resourceName: "ic_lock_outline")
|
||||
}
|
||||
}
|
||||
|
||||
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
|
||||
return stringsFrom(activityItems: activityItems).count > 0
|
||||
}
|
||||
|
||||
override func prepare(withActivityItems activityItems: [Any]) {
|
||||
let myFormattedSafetyNumbers = stringsFrom(activityItems: activityItems).first
|
||||
mySafetyNumbers = numericOnly(string: myFormattedSafetyNumbers)
|
||||
}
|
||||
|
||||
override func perform() {
|
||||
defer { activityDidFinish(true) }
|
||||
|
||||
let pasteboardString = numericOnly(string: UIPasteboard.general.string)
|
||||
guard (pasteboardString != nil && pasteboardString!.count == 60) else {
|
||||
Logger.warn("no valid safety numbers found in pasteboard: \(String(describing: pasteboardString))")
|
||||
let error = OWSErrorWithCodeDescription(OWSErrorCode.userError,
|
||||
NSLocalizedString("PRIVACY_VERIFICATION_FAILED_NO_SAFETY_NUMBERS_IN_CLIPBOARD", comment: "Alert body for user error"))
|
||||
|
||||
delegate.compareSafetyNumbersActivity(self, failedWithError: error)
|
||||
return
|
||||
}
|
||||
|
||||
let pasteboardSafetyNumbers = pasteboardString!
|
||||
|
||||
if pasteboardSafetyNumbers == mySafetyNumbers {
|
||||
Logger.info("successfully matched safety numbers. local numbers: \(String(describing: mySafetyNumbers)) pasteboard:\(pasteboardSafetyNumbers)")
|
||||
delegate.compareSafetyNumbersActivitySucceeded(activity: self)
|
||||
} else {
|
||||
Logger.warn("local numbers: \(String(describing: mySafetyNumbers)) didn't match pasteboard:\(pasteboardSafetyNumbers)")
|
||||
let error = OWSErrorWithCodeDescription(OWSErrorCode.privacyVerificationFailure,
|
||||
NSLocalizedString("PRIVACY_VERIFICATION_FAILED_MISMATCHED_SAFETY_NUMBERS_IN_CLIPBOARD", comment: "Alert body"))
|
||||
delegate.compareSafetyNumbersActivity(self, failedWithError: error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
func numericOnly(string: String?) -> String? {
|
||||
guard let string = string else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var numericOnly: String?
|
||||
if let regex = try? NSRegularExpression(pattern: "\\D", options: .caseInsensitive) {
|
||||
numericOnly = regex.stringByReplacingMatches(in: string, options: .withTransparentBounds, range: NSRange(location: 0, length: string.utf16.count), withTemplate: "")
|
||||
}
|
||||
|
||||
return numericOnly
|
||||
}
|
||||
|
||||
func stringsFrom(activityItems: [Any]) -> [String] {
|
||||
return activityItems.map { $0 as? String }.filter { $0 != nil }.map { $0! }
|
||||
}
|
||||
}
|
|
@ -1,162 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Contacts
|
||||
import SignalUtilitiesKit
|
||||
|
||||
class ContactCell: UITableViewCell {
|
||||
|
||||
public static let kSeparatorHInset: CGFloat = CGFloat(kAvatarDiameter) + 16 + 8
|
||||
|
||||
static let kAvatarSpacing: CGFloat = 6
|
||||
static let kAvatarDiameter: UInt = 40
|
||||
|
||||
let contactImageView: AvatarImageView
|
||||
let textStackView: UIStackView
|
||||
let titleLabel: UILabel
|
||||
var subtitleLabel: UILabel
|
||||
|
||||
var contact: Contact?
|
||||
var showsWhenSelected: Bool = false
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
self.contactImageView = AvatarImageView()
|
||||
self.textStackView = UIStackView()
|
||||
self.titleLabel = UILabel()
|
||||
self.titleLabel.font = UIFont.ows_dynamicTypeBody
|
||||
self.subtitleLabel = UILabel()
|
||||
self.subtitleLabel.font = UIFont.ows_dynamicTypeSubheadline
|
||||
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
selectionStyle = UITableViewCell.SelectionStyle.none
|
||||
|
||||
textStackView.axis = .vertical
|
||||
textStackView.addArrangedSubview(titleLabel)
|
||||
|
||||
contactImageView.autoSetDimensions(to: CGSize(width: CGFloat(ContactCell.kAvatarDiameter), height: CGFloat(ContactCell.kAvatarDiameter)))
|
||||
|
||||
let contentColumns: UIStackView = UIStackView(arrangedSubviews: [contactImageView, textStackView])
|
||||
contentColumns.axis = .horizontal
|
||||
contentColumns.spacing = ContactCell.kAvatarSpacing
|
||||
contentColumns.alignment = .center
|
||||
|
||||
self.contentView.addSubview(contentColumns)
|
||||
contentColumns.autoPinEdgesToSuperviewMargins()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.didChangePreferredContentSize), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
accessoryType = .none
|
||||
self.subtitleLabel.removeFromSuperview()
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
if showsWhenSelected {
|
||||
accessoryType = selected ? .checkmark : .none
|
||||
}
|
||||
}
|
||||
|
||||
@objc func didChangePreferredContentSize() {
|
||||
self.titleLabel.font = UIFont.ows_dynamicTypeBody
|
||||
self.subtitleLabel.font = UIFont.ows_dynamicTypeSubheadline
|
||||
}
|
||||
|
||||
func configure(contact: Contact, subtitleType: SubtitleCellValue, showsWhenSelected: Bool, contactsManager: OWSContactsManager) {
|
||||
|
||||
OWSTableItem.configureCell(self)
|
||||
|
||||
self.contact = contact
|
||||
self.showsWhenSelected = showsWhenSelected
|
||||
|
||||
self.titleLabel.textColor = Theme.primaryColor
|
||||
self.subtitleLabel.textColor = Theme.secondaryColor
|
||||
|
||||
let cnContact = contactsManager.cnContact(withId: contact.cnContactId)
|
||||
titleLabel.attributedText = cnContact?.formattedFullName(font: titleLabel.font)
|
||||
updateSubtitle(subtitleType: subtitleType, contact: contact)
|
||||
|
||||
if let contactImage = contactsManager.avatarImage(forCNContactId: contact.cnContactId) {
|
||||
contactImageView.image = contactImage
|
||||
} else {
|
||||
let contactIdForDeterminingBackgroundColor: String
|
||||
if let signalId = contact.parsedPhoneNumbers.first?.toE164() {
|
||||
contactIdForDeterminingBackgroundColor = signalId
|
||||
} else {
|
||||
contactIdForDeterminingBackgroundColor = contact.fullName
|
||||
}
|
||||
|
||||
let avatarBuilder = OWSContactAvatarBuilder(nonSignalName: contact.fullName,
|
||||
colorSeed: contactIdForDeterminingBackgroundColor,
|
||||
diameter: ContactCell.kAvatarDiameter)
|
||||
|
||||
contactImageView.image = avatarBuilder.build()
|
||||
}
|
||||
}
|
||||
|
||||
func updateSubtitle(subtitleType: SubtitleCellValue, contact: Contact) {
|
||||
switch subtitleType {
|
||||
case .none:
|
||||
assert(self.subtitleLabel.superview == nil)
|
||||
break
|
||||
case .phoneNumber:
|
||||
self.textStackView.addArrangedSubview(self.subtitleLabel)
|
||||
|
||||
if let firstPhoneNumber = contact.userTextPhoneNumbers.first {
|
||||
self.subtitleLabel.text = firstPhoneNumber
|
||||
} else {
|
||||
self.subtitleLabel.text = NSLocalizedString("CONTACT_PICKER_NO_PHONE_NUMBERS_AVAILABLE", comment: "table cell subtitle when contact card has no known phone number")
|
||||
}
|
||||
case .email:
|
||||
self.textStackView.addArrangedSubview(self.subtitleLabel)
|
||||
|
||||
if let firstEmail = contact.emails.first {
|
||||
self.subtitleLabel.text = firstEmail
|
||||
} else {
|
||||
self.subtitleLabel.text = NSLocalizedString("CONTACT_PICKER_NO_EMAILS_AVAILABLE", comment: "table cell subtitle when contact card has no email")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension CNContact {
|
||||
/**
|
||||
* Bold the sorting portion of the name. e.g. if we sort by family name, bold the family name.
|
||||
*/
|
||||
func formattedFullName(font: UIFont) -> NSAttributedString? {
|
||||
let keyToHighlight = ContactSortOrder == .familyName ? CNContactFamilyNameKey : CNContactGivenNameKey
|
||||
|
||||
let boldDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold)
|
||||
let boldAttributes = [
|
||||
NSAttributedString.Key.font: UIFont(descriptor: boldDescriptor!, size: 0)
|
||||
]
|
||||
|
||||
if let attributedName = CNContactFormatter.attributedString(from: self, style: .fullName, defaultAttributes: nil) {
|
||||
let highlightedName = attributedName.mutableCopy() as! NSMutableAttributedString
|
||||
highlightedName.enumerateAttributes(in: NSRange(location: 0, length: highlightedName.length), options: [], using: { (attrs, range, _) in
|
||||
if let property = attrs[NSAttributedString.Key(rawValue: CNContactPropertyAttribute)] as? String, property == keyToHighlight {
|
||||
highlightedName.addAttributes(boldAttributes, range: range)
|
||||
}
|
||||
})
|
||||
return highlightedName
|
||||
}
|
||||
|
||||
if let emailAddress = self.emailAddresses.first?.value {
|
||||
return NSAttributedString(string: emailAddress as String, attributes: boldAttributes)
|
||||
}
|
||||
|
||||
if let phoneNumber = self.phoneNumbers.first?.value.stringValue {
|
||||
return NSAttributedString(string: phoneNumber, attributes: boldAttributes)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -1,215 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalUtilitiesKit
|
||||
import ContactsUI
|
||||
import MessageUI
|
||||
|
||||
@objc
|
||||
public protocol ContactShareViewHelperDelegate: class {
|
||||
func didCreateOrEditContact()
|
||||
}
|
||||
|
||||
@objc
|
||||
public class ContactShareViewHelper: NSObject, CNContactViewControllerDelegate {
|
||||
|
||||
@objc
|
||||
weak var delegate: ContactShareViewHelperDelegate?
|
||||
|
||||
let contactsManager: OWSContactsManager
|
||||
|
||||
@objc
|
||||
public required init(contactsManager: OWSContactsManager) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
self.contactsManager = contactsManager
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@objc
|
||||
public func sendMessage(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
presentThreadAndPeform(action: .compose, contactShare: contactShare, fromViewController: fromViewController)
|
||||
}
|
||||
|
||||
@objc
|
||||
public func audioCall(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
presentThreadAndPeform(action: .audioCall, contactShare: contactShare, fromViewController: fromViewController)
|
||||
}
|
||||
|
||||
@objc
|
||||
public func videoCall(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
presentThreadAndPeform(action: .videoCall, contactShare: contactShare, fromViewController: fromViewController)
|
||||
}
|
||||
|
||||
private func presentThreadAndPeform(action: ConversationViewAction, contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
// TODO: We're taking the first Signal account id. We might
|
||||
// want to let the user select if there's more than one.
|
||||
let phoneNumbers = contactShare.systemContactsWithSignalAccountPhoneNumbers(contactsManager)
|
||||
guard phoneNumbers.count > 0 else {
|
||||
owsFailDebug("missing Signal recipient id.")
|
||||
return
|
||||
}
|
||||
guard phoneNumbers.count > 1 else {
|
||||
let recipientId = phoneNumbers.first!
|
||||
SignalApp.shared().presentConversation(forRecipientId: recipientId, action: action, animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
showPhoneNumberPicker(phoneNumbers: phoneNumbers, fromViewController: fromViewController, completion: { (recipientId) in
|
||||
SignalApp.shared().presentConversation(forRecipientId: recipientId, action: action, animated: true)
|
||||
})
|
||||
}
|
||||
|
||||
@objc
|
||||
public func showInviteContact(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
guard MFMessageComposeViewController.canSendText() else {
|
||||
Logger.info("Device cannot send text")
|
||||
OWSAlerts.showErrorAlert(message: NSLocalizedString("UNSUPPORTED_FEATURE_ERROR", comment: ""))
|
||||
return
|
||||
}
|
||||
let phoneNumbers = contactShare.e164PhoneNumbers()
|
||||
guard phoneNumbers.count > 0 else {
|
||||
owsFailDebug("no phone numbers.")
|
||||
return
|
||||
}
|
||||
|
||||
let inviteFlow = InviteFlow(presentingViewController: fromViewController)
|
||||
inviteFlow.sendSMSTo(phoneNumbers: phoneNumbers)
|
||||
}
|
||||
|
||||
@objc
|
||||
func showAddToContacts(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("CONVERSATION_SETTINGS_NEW_CONTACT",
|
||||
comment: "Label for 'new contact' button in conversation settings view."),
|
||||
style: .default) { _ in
|
||||
self.didPressCreateNewContact(contactShare: contactShare, fromViewController: fromViewController)
|
||||
})
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("CONVERSATION_SETTINGS_ADD_TO_EXISTING_CONTACT",
|
||||
comment: "Label for 'new contact' button in conversation settings view."),
|
||||
style: .default) { _ in
|
||||
self.didPressAddToExistingContact(contactShare: contactShare, fromViewController: fromViewController)
|
||||
})
|
||||
actionSheet.addAction(OWSAlerts.cancelAction)
|
||||
|
||||
fromViewController.presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
private func showPhoneNumberPicker(phoneNumbers: [String], fromViewController: UIViewController, completion :@escaping ((String) -> Void)) {
|
||||
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
for phoneNumber in phoneNumbers {
|
||||
actionSheet.addAction(UIAlertAction(title: PhoneNumber.bestEffortLocalizedPhoneNumber(withE164: phoneNumber),
|
||||
style: .default) { _ in
|
||||
completion(phoneNumber)
|
||||
})
|
||||
}
|
||||
actionSheet.addAction(OWSAlerts.cancelAction)
|
||||
|
||||
fromViewController.presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
func didPressCreateNewContact(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
presentNewContactView(contactShare: contactShare, fromViewController: fromViewController)
|
||||
}
|
||||
|
||||
func didPressAddToExistingContact(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
Logger.info("")
|
||||
|
||||
presentSelectAddToExistingContactView(contactShare: contactShare, fromViewController: fromViewController)
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private func presentNewContactView(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
guard contactsManager.supportsContactEditing else {
|
||||
owsFailDebug("Contact editing not supported")
|
||||
return
|
||||
}
|
||||
|
||||
guard let systemContact = OWSContacts.systemContact(for: contactShare.dbRecord, imageData: contactShare.avatarImageData) else {
|
||||
owsFailDebug("Could not derive system contact.")
|
||||
return
|
||||
}
|
||||
|
||||
guard contactsManager.isSystemContactsAuthorized else {
|
||||
ContactsViewHelper.presentMissingContactAccessAlertController(from: fromViewController)
|
||||
return
|
||||
}
|
||||
|
||||
let contactViewController = CNContactViewController(forNewContact: systemContact)
|
||||
contactViewController.delegate = self
|
||||
contactViewController.allowsActions = false
|
||||
contactViewController.allowsEditing = true
|
||||
contactViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(title: CommonStrings.cancelButton,
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(didFinishEditingContact))
|
||||
|
||||
let modal = OWSNavigationController(rootViewController: contactViewController)
|
||||
fromViewController.present(modal, animated: true)
|
||||
}
|
||||
|
||||
private func presentSelectAddToExistingContactView(contactShare: ContactShareViewModel, fromViewController: UIViewController) {
|
||||
guard contactsManager.supportsContactEditing else {
|
||||
owsFailDebug("Contact editing not supported")
|
||||
return
|
||||
}
|
||||
|
||||
guard contactsManager.isSystemContactsAuthorized else {
|
||||
ContactsViewHelper.presentMissingContactAccessAlertController(from: fromViewController)
|
||||
return
|
||||
}
|
||||
|
||||
guard let navigationController = fromViewController.navigationController else {
|
||||
owsFailDebug("missing navigationController")
|
||||
return
|
||||
}
|
||||
|
||||
let viewController = AddContactShareToExistingContactViewController(contactShare: contactShare)
|
||||
navigationController.pushViewController(viewController, animated: true)
|
||||
}
|
||||
|
||||
// MARK: - CNContactViewControllerDelegate
|
||||
|
||||
@objc public func contactViewController(_ viewController: CNContactViewController, didCompleteWith contact: CNContact?) {
|
||||
Logger.info("")
|
||||
|
||||
guard let delegate = delegate else {
|
||||
owsFailDebug("missing delegate")
|
||||
return
|
||||
}
|
||||
|
||||
delegate.didCreateOrEditContact()
|
||||
}
|
||||
|
||||
@objc public func didFinishEditingContact() {
|
||||
Logger.info("")
|
||||
|
||||
guard let delegate = delegate else {
|
||||
owsFailDebug("missing delegate")
|
||||
return
|
||||
}
|
||||
|
||||
delegate.didCreateOrEditContact()
|
||||
}
|
||||
}
|
|
@ -1,679 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalUtilitiesKit
|
||||
import SignalUtilitiesKit
|
||||
import Reachability
|
||||
import ContactsUI
|
||||
import MessageUI
|
||||
|
||||
class ContactViewController: OWSViewController, ContactShareViewHelperDelegate {
|
||||
|
||||
enum ContactViewMode {
|
||||
case systemContactWithSignal,
|
||||
systemContactWithoutSignal,
|
||||
nonSystemContact,
|
||||
noPhoneNumber,
|
||||
unknown
|
||||
}
|
||||
|
||||
private var hasLoadedView = false
|
||||
|
||||
private var viewMode = ContactViewMode.unknown {
|
||||
didSet {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
if oldValue != viewMode && hasLoadedView {
|
||||
updateContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let contactsManager: OWSContactsManager
|
||||
|
||||
private var reachability: Reachability?
|
||||
|
||||
private let contactShare: ContactShareViewModel
|
||||
|
||||
private var contactShareViewHelper: ContactShareViewHelper
|
||||
|
||||
private weak var postDismissNavigationController: UINavigationController?
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
@available(*, unavailable, message: "use init(call:) constructor instead.")
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
@objc
|
||||
required init(contactShare: ContactShareViewModel) {
|
||||
contactsManager = Environment.shared.contactsManager
|
||||
self.contactShare = contactShare
|
||||
self.contactShareViewHelper = ContactShareViewHelper(contactsManager: contactsManager)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
contactShareViewHelper.delegate = self
|
||||
|
||||
updateMode()
|
||||
|
||||
NotificationCenter.default.addObserver(forName: .OWSContactsManagerSignalAccountsDidChange, object: nil, queue: nil) { [weak self] _ in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.updateMode()
|
||||
}
|
||||
|
||||
reachability = Reachability.forInternetConnection()
|
||||
|
||||
NotificationCenter.default.addObserver(forName: .reachabilityChanged, object: nil, queue: nil) { [weak self] _ in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.updateMode()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - View Lifecycle
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
// self.navigationController is nil in viewWillDisappear when transition via message/call buttons
|
||||
// so we maintain our own reference to restore the navigation bars.
|
||||
postDismissNavigationController = navigationController
|
||||
navigationController.isNavigationBarHidden = true
|
||||
|
||||
contactsManager.requestSystemContactsOnce(completion: { [weak self] _ in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.updateMode()
|
||||
})
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
if self.presentedViewController == nil {
|
||||
// No need to do this when we're disappearing due to a modal presentation.
|
||||
// We'll eventually return to to this view and need to hide again. But also, there is a visible
|
||||
// animation glitch where the navigation bar for this view controller starts to appear while
|
||||
// the whole nav stack is about to be obscured by the modal we are presenting.
|
||||
guard let postDismissNavigationController = self.postDismissNavigationController else {
|
||||
owsFailDebug("postDismissNavigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
postDismissNavigationController.setNavigationBarHidden(false, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
super.loadView()
|
||||
|
||||
self.view.preservesSuperviewLayoutMargins = false
|
||||
self.view.backgroundColor = heroBackgroundColor()
|
||||
|
||||
updateContent()
|
||||
|
||||
hasLoadedView = true
|
||||
}
|
||||
|
||||
private func updateMode() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard contactShare.e164PhoneNumbers().count > 0 else {
|
||||
viewMode = .noPhoneNumber
|
||||
return
|
||||
}
|
||||
if systemContactsWithSignalAccountsForContact().count > 0 {
|
||||
viewMode = .systemContactWithSignal
|
||||
return
|
||||
}
|
||||
if systemContactsForContact().count > 0 {
|
||||
viewMode = .systemContactWithoutSignal
|
||||
return
|
||||
}
|
||||
|
||||
viewMode = .nonSystemContact
|
||||
}
|
||||
|
||||
private func systemContactsWithSignalAccountsForContact() -> [String] {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
return contactShare.systemContactsWithSignalAccountPhoneNumbers(contactsManager)
|
||||
}
|
||||
|
||||
private func systemContactsForContact() -> [String] {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
return contactShare.systemContactPhoneNumbers(contactsManager)
|
||||
}
|
||||
|
||||
private func updateContent() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let rootView = self.view else {
|
||||
owsFailDebug("missing root view.")
|
||||
return
|
||||
}
|
||||
|
||||
for subview in rootView.subviews {
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
|
||||
let topView = createTopView()
|
||||
rootView.addSubview(topView)
|
||||
topView.autoPinEdge(.top, to: .top, of: view)
|
||||
topView.autoPinWidthToSuperview()
|
||||
|
||||
// This view provides a background "below the fold".
|
||||
let bottomView = UIView.container()
|
||||
bottomView.backgroundColor = Theme.backgroundColor
|
||||
self.view.addSubview(bottomView)
|
||||
bottomView.layoutMargins = .zero
|
||||
bottomView.autoPinWidthToSuperview()
|
||||
bottomView.autoPinEdge(.top, to: .bottom, of: topView)
|
||||
bottomView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
|
||||
let scrollView = UIScrollView()
|
||||
scrollView.preservesSuperviewLayoutMargins = false
|
||||
self.view.addSubview(scrollView)
|
||||
scrollView.layoutMargins = .zero
|
||||
scrollView.autoPinWidthToSuperview()
|
||||
scrollView.autoPinEdge(.top, to: .bottom, of: topView)
|
||||
scrollView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
|
||||
let fieldsView = createFieldsView()
|
||||
|
||||
scrollView.addSubview(fieldsView)
|
||||
fieldsView.autoPinLeadingToSuperviewMargin()
|
||||
fieldsView.autoPinTrailingToSuperviewMargin()
|
||||
fieldsView.autoPinEdge(toSuperviewEdge: .top)
|
||||
fieldsView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
}
|
||||
|
||||
private func heroBackgroundColor() -> UIColor {
|
||||
return (Theme.isDarkThemeEnabled
|
||||
? UIColor(rgbHex: 0x272727)
|
||||
: UIColor(rgbHex: 0xefeff4))
|
||||
}
|
||||
|
||||
private func createTopView() -> UIView {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let topView = UIView.container()
|
||||
topView.backgroundColor = heroBackgroundColor()
|
||||
topView.preservesSuperviewLayoutMargins = false
|
||||
|
||||
// Back Button
|
||||
let backButtonSize = CGFloat(50)
|
||||
let backButton = TappableView(actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressDismiss()
|
||||
})
|
||||
backButton.autoSetDimension(.width, toSize: backButtonSize)
|
||||
backButton.autoSetDimension(.height, toSize: backButtonSize)
|
||||
topView.addSubview(backButton)
|
||||
backButton.autoPinEdge(toSuperviewEdge: .top)
|
||||
backButton.autoPinLeadingToSuperviewMargin()
|
||||
|
||||
let backIconName = (CurrentAppContext().isRTL ? "system_disclosure_indicator" : "system_disclosure_indicator_rtl")
|
||||
guard let backIconImage = UIImage(named: backIconName) else {
|
||||
owsFailDebug("missing icon.")
|
||||
return topView
|
||||
}
|
||||
let backIconView = UIImageView(image: backIconImage.withRenderingMode(.alwaysTemplate))
|
||||
backIconView.contentMode = .scaleAspectFit
|
||||
backIconView.tintColor = Theme.primaryColor.withAlphaComponent(0.6)
|
||||
backButton.addSubview(backIconView)
|
||||
backIconView.autoCenterInSuperview()
|
||||
|
||||
let avatarSize: CGFloat = 100
|
||||
let avatarView = AvatarImageView()
|
||||
avatarView.image = contactShare.getAvatarImage(diameter: avatarSize, contactsManager: contactsManager)
|
||||
topView.addSubview(avatarView)
|
||||
avatarView.autoPinEdge(toSuperviewEdge: .top, withInset: 20)
|
||||
avatarView.autoHCenterInSuperview()
|
||||
avatarView.autoSetDimension(.width, toSize: avatarSize)
|
||||
avatarView.autoSetDimension(.height, toSize: avatarSize)
|
||||
|
||||
let nameLabel = UILabel()
|
||||
nameLabel.text = contactShare.displayName
|
||||
nameLabel.font = UIFont.ows_dynamicTypeTitle1
|
||||
nameLabel.textColor = Theme.primaryColor
|
||||
nameLabel.lineBreakMode = .byTruncatingTail
|
||||
nameLabel.textAlignment = .center
|
||||
topView.addSubview(nameLabel)
|
||||
nameLabel.autoPinEdge(.top, to: .bottom, of: avatarView, withOffset: 10)
|
||||
nameLabel.autoPinLeadingToSuperviewMargin(withInset: hMargin)
|
||||
nameLabel.autoPinTrailingToSuperviewMargin(withInset: hMargin)
|
||||
|
||||
var lastView: UIView = nameLabel
|
||||
|
||||
for phoneNumber in systemContactsWithSignalAccountsForContact() {
|
||||
let phoneNumberLabel = UILabel()
|
||||
phoneNumberLabel.text = PhoneNumber.bestEffortLocalizedPhoneNumber(withE164: phoneNumber)
|
||||
phoneNumberLabel.font = UIFont.ows_dynamicTypeFootnote
|
||||
phoneNumberLabel.textColor = Theme.primaryColor
|
||||
phoneNumberLabel.lineBreakMode = .byTruncatingTail
|
||||
phoneNumberLabel.textAlignment = .center
|
||||
topView.addSubview(phoneNumberLabel)
|
||||
phoneNumberLabel.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 5)
|
||||
phoneNumberLabel.autoPinLeadingToSuperviewMargin(withInset: hMargin)
|
||||
phoneNumberLabel.autoPinTrailingToSuperviewMargin(withInset: hMargin)
|
||||
lastView = phoneNumberLabel
|
||||
}
|
||||
|
||||
switch viewMode {
|
||||
case .systemContactWithSignal:
|
||||
// Show actions buttons for system contacts with a Signal account.
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .horizontal
|
||||
stackView.distribution = .fillEqually
|
||||
stackView.addArrangedSubview(createCircleActionButton(text: NSLocalizedString("ACTION_SEND_MESSAGE",
|
||||
comment: "Label for 'send message' button in contact view."),
|
||||
imageName: "contact_view_message",
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressSendMessage()
|
||||
}))
|
||||
stackView.addArrangedSubview(createCircleActionButton(text: NSLocalizedString("ACTION_AUDIO_CALL",
|
||||
comment: "Label for 'audio call' button in contact view."),
|
||||
imageName: "contact_view_audio_call",
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressAudioCall()
|
||||
}))
|
||||
stackView.addArrangedSubview(createCircleActionButton(text: NSLocalizedString("ACTION_VIDEO_CALL",
|
||||
comment: "Label for 'video call' button in contact view."),
|
||||
imageName: "contact_view_video_call",
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressVideoCall()
|
||||
}))
|
||||
topView.addSubview(stackView)
|
||||
stackView.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 20)
|
||||
stackView.autoPinLeadingToSuperviewMargin(withInset: hMargin)
|
||||
stackView.autoPinTrailingToSuperviewMargin(withInset: hMargin)
|
||||
lastView = stackView
|
||||
case .systemContactWithoutSignal:
|
||||
// Show invite button for system contacts without a Signal account.
|
||||
let inviteButton = createLargePillButton(text: NSLocalizedString("ACTION_INVITE",
|
||||
comment: "Label for 'invite' button in contact view."),
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressInvite()
|
||||
})
|
||||
topView.addSubview(inviteButton)
|
||||
inviteButton.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 20)
|
||||
inviteButton.autoPinLeadingToSuperviewMargin(withInset: 55)
|
||||
inviteButton.autoPinTrailingToSuperviewMargin(withInset: 55)
|
||||
lastView = inviteButton
|
||||
case .nonSystemContact:
|
||||
// Show no action buttons for non-system contacts.
|
||||
break
|
||||
case .noPhoneNumber:
|
||||
// Show no action buttons for contacts without a phone number.
|
||||
break
|
||||
case .unknown:
|
||||
let activityIndicator = UIActivityIndicatorView(style: .whiteLarge)
|
||||
topView.addSubview(activityIndicator)
|
||||
activityIndicator.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 10)
|
||||
activityIndicator.autoHCenterInSuperview()
|
||||
lastView = activityIndicator
|
||||
break
|
||||
}
|
||||
|
||||
// Always show "add to contacts" button.
|
||||
let addToContactsButton = createLargePillButton(text: NSLocalizedString("CONVERSATION_VIEW_ADD_TO_CONTACTS_OFFER",
|
||||
comment: "Message shown in conversation view that offers to add an unknown user to your phone's contacts."),
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressAddToContacts()
|
||||
})
|
||||
topView.addSubview(addToContactsButton)
|
||||
addToContactsButton.autoPinEdge(.top, to: .bottom, of: lastView, withOffset: 20)
|
||||
addToContactsButton.autoPinLeadingToSuperviewMargin(withInset: 55)
|
||||
addToContactsButton.autoPinTrailingToSuperviewMargin(withInset: 55)
|
||||
lastView = addToContactsButton
|
||||
|
||||
lastView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 15)
|
||||
|
||||
return topView
|
||||
}
|
||||
|
||||
private func createFieldsView() -> UIView {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
var rows = [UIView]()
|
||||
|
||||
// TODO: Not designed yet.
|
||||
// if viewMode == .systemContactWithSignal ||
|
||||
// viewMode == .systemContactWithoutSignal {
|
||||
// addRow(createActionRow(labelText:NSLocalizedString("ACTION_SHARE_CONTACT",
|
||||
// comment:"Label for 'share contact' button."),
|
||||
// action:#selector(didPressShareContact)))
|
||||
// }
|
||||
|
||||
if let organizationName = contactShare.name.organizationName?.ows_stripped() {
|
||||
if (contactShare.name.hasAnyNamePart() &&
|
||||
organizationName.count > 0) {
|
||||
rows.append(ContactFieldView.contactFieldView(forOrganizationName: organizationName,
|
||||
layoutMargins: UIEdgeInsets(top: 5, left: hMargin, bottom: 5, right: hMargin)))
|
||||
}
|
||||
}
|
||||
|
||||
for phoneNumber in contactShare.phoneNumbers {
|
||||
rows.append(ContactFieldView.contactFieldView(forPhoneNumber: phoneNumber,
|
||||
layoutMargins: UIEdgeInsets(top: 5, left: hMargin, bottom: 5, right: hMargin),
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressPhoneNumber(phoneNumber: phoneNumber)
|
||||
}))
|
||||
}
|
||||
|
||||
for email in contactShare.emails {
|
||||
rows.append(ContactFieldView.contactFieldView(forEmail: email,
|
||||
layoutMargins: UIEdgeInsets(top: 5, left: hMargin, bottom: 5, right: hMargin),
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressEmail(email: email)
|
||||
}))
|
||||
}
|
||||
|
||||
for address in contactShare.addresses {
|
||||
rows.append(ContactFieldView.contactFieldView(forAddress: address,
|
||||
layoutMargins: UIEdgeInsets(top: 5, left: hMargin, bottom: 5, right: hMargin),
|
||||
actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.didPressAddress(address: address)
|
||||
}))
|
||||
}
|
||||
|
||||
return ContactFieldView(rows: rows, hMargin: hMargin)
|
||||
}
|
||||
|
||||
private let hMargin = CGFloat(16)
|
||||
|
||||
private func createActionRow(labelText: String, action: Selector) -> UIView {
|
||||
let row = UIView()
|
||||
row.layoutMargins.left = 0
|
||||
row.layoutMargins.right = 0
|
||||
row.isUserInteractionEnabled = true
|
||||
row.addGestureRecognizer(UITapGestureRecognizer(target: self, action: action))
|
||||
|
||||
let label = UILabel()
|
||||
label.text = labelText
|
||||
label.font = UIFont.ows_dynamicTypeBody
|
||||
label.textColor = UIColor.ows_materialBlue
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
row.addSubview(label)
|
||||
label.autoPinTopToSuperviewMargin()
|
||||
label.autoPinBottomToSuperviewMargin()
|
||||
label.autoPinLeadingToSuperviewMargin(withInset: hMargin)
|
||||
label.autoPinTrailingToSuperviewMargin(withInset: hMargin)
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
// TODO: Use real assets.
|
||||
private func createCircleActionButton(text: String, imageName: String, actionBlock : @escaping () -> Void) -> UIView {
|
||||
let buttonSize = CGFloat(50)
|
||||
|
||||
let button = TappableView(actionBlock: actionBlock)
|
||||
button.layoutMargins = .zero
|
||||
button.autoSetDimension(.width, toSize: buttonSize, relation: .greaterThanOrEqual)
|
||||
|
||||
let circleView = UIView()
|
||||
circleView.backgroundColor = Theme.backgroundColor
|
||||
circleView.autoSetDimension(.width, toSize: buttonSize)
|
||||
circleView.autoSetDimension(.height, toSize: buttonSize)
|
||||
circleView.layer.cornerRadius = buttonSize * 0.5
|
||||
button.addSubview(circleView)
|
||||
circleView.autoPinEdge(toSuperviewEdge: .top)
|
||||
circleView.autoHCenterInSuperview()
|
||||
|
||||
guard let image = UIImage(named: imageName) else {
|
||||
owsFailDebug("missing image.")
|
||||
return button
|
||||
}
|
||||
let imageView = UIImageView(image: image.withRenderingMode(.alwaysTemplate))
|
||||
imageView.tintColor = Theme.primaryColor.withAlphaComponent(0.6)
|
||||
circleView.addSubview(imageView)
|
||||
imageView.autoCenterInSuperview()
|
||||
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.font = UIFont.ows_dynamicTypeCaption2
|
||||
label.textColor = Theme.primaryColor
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.textAlignment = .center
|
||||
button.addSubview(label)
|
||||
label.autoPinEdge(.top, to: .bottom, of: circleView, withOffset: 3)
|
||||
label.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
label.autoPinLeadingToSuperviewMargin()
|
||||
label.autoPinTrailingToSuperviewMargin()
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
private func createLargePillButton(text: String, actionBlock : @escaping () -> Void) -> UIView {
|
||||
let button = TappableView(actionBlock: actionBlock)
|
||||
button.backgroundColor = Theme.backgroundColor
|
||||
button.layoutMargins = .zero
|
||||
button.autoSetDimension(.height, toSize: 45)
|
||||
button.layer.cornerRadius = 5
|
||||
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.font = UIFont.ows_dynamicTypeBody
|
||||
label.textColor = UIColor.ows_materialBlue
|
||||
label.lineBreakMode = .byTruncatingTail
|
||||
label.textAlignment = .center
|
||||
button.addSubview(label)
|
||||
label.autoPinLeadingToSuperviewMargin(withInset: 20)
|
||||
label.autoPinTrailingToSuperviewMargin(withInset: 20)
|
||||
label.autoVCenterInSuperview()
|
||||
label.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual)
|
||||
label.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual)
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
func didPressShareContact(sender: UIGestureRecognizer) {
|
||||
Logger.info("")
|
||||
|
||||
guard sender.state == .recognized else {
|
||||
return
|
||||
}
|
||||
// TODO:
|
||||
}
|
||||
|
||||
func didPressSendMessage() {
|
||||
Logger.info("")
|
||||
|
||||
self.contactShareViewHelper.sendMessage(contactShare: self.contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didPressAudioCall() {
|
||||
Logger.info("")
|
||||
|
||||
self.contactShareViewHelper.audioCall(contactShare: self.contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didPressVideoCall() {
|
||||
Logger.info("")
|
||||
|
||||
self.contactShareViewHelper.videoCall(contactShare: self.contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didPressInvite() {
|
||||
Logger.info("")
|
||||
|
||||
self.contactShareViewHelper.showInviteContact(contactShare: self.contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didPressAddToContacts() {
|
||||
Logger.info("")
|
||||
|
||||
self.contactShareViewHelper.showAddToContacts(contactShare: self.contactShare, fromViewController: self)
|
||||
}
|
||||
|
||||
func didPressDismiss() {
|
||||
Logger.info("")
|
||||
|
||||
guard let navigationController = self.navigationController else {
|
||||
owsFailDebug("navigationController was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
navigationController.popViewController(animated: true)
|
||||
}
|
||||
|
||||
func didPressPhoneNumber(phoneNumber: OWSContactPhoneNumber) {
|
||||
Logger.info("")
|
||||
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
if let e164 = phoneNumber.tryToConvertToE164() {
|
||||
if contactShare.systemContactsWithSignalAccountPhoneNumbers(contactsManager).contains(e164) {
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ACTION_SEND_MESSAGE",
|
||||
comment: "Label for 'send message' button in contact view."),
|
||||
style: .default) { _ in
|
||||
SignalApp.shared().presentConversation(forRecipientId: e164, action: .compose, animated: true)
|
||||
})
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ACTION_AUDIO_CALL",
|
||||
comment: "Label for 'audio call' button in contact view."),
|
||||
style: .default) { _ in
|
||||
SignalApp.shared().presentConversation(forRecipientId: e164, action: .audioCall, animated: true)
|
||||
})
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("ACTION_VIDEO_CALL",
|
||||
comment: "Label for 'video call' button in contact view."),
|
||||
style: .default) { _ in
|
||||
SignalApp.shared().presentConversation(forRecipientId: e164, action: .videoCall, animated: true)
|
||||
})
|
||||
} else {
|
||||
// TODO: We could offer callPhoneNumberWithSystemCall.
|
||||
}
|
||||
}
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("EDIT_ITEM_COPY_ACTION",
|
||||
comment: "Short name for edit menu item to copy contents of media message."),
|
||||
style: .default) { _ in
|
||||
UIPasteboard.general.string = phoneNumber.phoneNumber
|
||||
})
|
||||
actionSheet.addAction(OWSAlerts.cancelAction)
|
||||
presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
func callPhoneNumberWithSystemCall(phoneNumber: OWSContactPhoneNumber) {
|
||||
Logger.info("")
|
||||
|
||||
guard let url = NSURL(string: "tel:\(phoneNumber.phoneNumber)") else {
|
||||
owsFailDebug("could not open phone number.")
|
||||
return
|
||||
}
|
||||
UIApplication.shared.openURL(url as URL)
|
||||
}
|
||||
|
||||
func didPressEmail(email: OWSContactEmail) {
|
||||
Logger.info("")
|
||||
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("CONTACT_VIEW_OPEN_EMAIL_IN_EMAIL_APP",
|
||||
comment: "Label for 'open email in email app' button in contact view."),
|
||||
style: .default) { [weak self] _ in
|
||||
self?.openEmailInEmailApp(email: email)
|
||||
})
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("EDIT_ITEM_COPY_ACTION",
|
||||
comment: "Short name for edit menu item to copy contents of media message."),
|
||||
style: .default) { _ in
|
||||
UIPasteboard.general.string = email.email
|
||||
})
|
||||
actionSheet.addAction(OWSAlerts.cancelAction)
|
||||
presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
func openEmailInEmailApp(email: OWSContactEmail) {
|
||||
Logger.info("")
|
||||
|
||||
guard let url = NSURL(string: "mailto:\(email.email)") else {
|
||||
owsFailDebug("could not open email.")
|
||||
return
|
||||
}
|
||||
UIApplication.shared.openURL(url as URL)
|
||||
}
|
||||
|
||||
func didPressAddress(address: OWSContactAddress) {
|
||||
Logger.info("")
|
||||
|
||||
let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("CONTACT_VIEW_OPEN_ADDRESS_IN_MAPS_APP",
|
||||
comment: "Label for 'open address in maps app' button in contact view."),
|
||||
style: .default) { [weak self] _ in
|
||||
self?.openAddressInMaps(address: address)
|
||||
})
|
||||
actionSheet.addAction(UIAlertAction(title: NSLocalizedString("EDIT_ITEM_COPY_ACTION",
|
||||
comment: "Short name for edit menu item to copy contents of media message."),
|
||||
style: .default) { [weak self] _ in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
UIPasteboard.general.string = strongSelf.formatAddressForQuery(address: address)
|
||||
})
|
||||
actionSheet.addAction(OWSAlerts.cancelAction)
|
||||
presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
func openAddressInMaps(address: OWSContactAddress) {
|
||||
Logger.info("")
|
||||
|
||||
let mapAddress = formatAddressForQuery(address: address)
|
||||
guard let escapedMapAddress = mapAddress.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
|
||||
owsFailDebug("could not open address.")
|
||||
return
|
||||
}
|
||||
// Note that we use "q" (i.e. query) rather than "address" since we can't assume
|
||||
// this is a well-formed address.
|
||||
guard let url = URL(string: "http://maps.apple.com/?q=\(escapedMapAddress)") else {
|
||||
owsFailDebug("could not open address.")
|
||||
return
|
||||
}
|
||||
|
||||
UIApplication.shared.openURL(url as URL)
|
||||
}
|
||||
|
||||
func formatAddressForQuery(address: OWSContactAddress) -> String {
|
||||
Logger.info("")
|
||||
|
||||
// Open address in Apple Maps app.
|
||||
var addressParts = [String]()
|
||||
let addAddressPart: ((String?) -> Void) = { (part) in
|
||||
guard let part = part else {
|
||||
return
|
||||
}
|
||||
guard part.count > 0 else {
|
||||
return
|
||||
}
|
||||
addressParts.append(part)
|
||||
}
|
||||
addAddressPart(address.street)
|
||||
addAddressPart(address.neighborhood)
|
||||
addAddressPart(address.city)
|
||||
addAddressPart(address.region)
|
||||
addAddressPart(address.postcode)
|
||||
addAddressPart(address.country)
|
||||
return addressParts.joined(separator: ", ")
|
||||
}
|
||||
|
||||
// MARK: - ContactShareViewHelperDelegate
|
||||
|
||||
public func didCreateOrEditContact() {
|
||||
Logger.info("")
|
||||
updateContent()
|
||||
|
||||
self.dismiss(animated: true)
|
||||
}
|
||||
}
|
|
@ -1,404 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
// Originally based on EPContacts
|
||||
//
|
||||
// Created by Prabaharan Elangovan on 12/10/15.
|
||||
// Parts Copyright © 2015 Prabaharan Elangovan. All rights reserved
|
||||
|
||||
import UIKit
|
||||
import Contacts
|
||||
import SignalUtilitiesKit
|
||||
|
||||
@objc
|
||||
public protocol ContactsPickerDelegate: class {
|
||||
func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError)
|
||||
func contactsPickerDidCancel(_: ContactsPicker)
|
||||
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact)
|
||||
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact])
|
||||
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool
|
||||
}
|
||||
|
||||
@objc
|
||||
public enum SubtitleCellValue: Int {
|
||||
case phoneNumber, email, none
|
||||
}
|
||||
|
||||
@objc
|
||||
public class ContactsPicker: OWSViewController, UITableViewDelegate, UITableViewDataSource, UISearchBarDelegate {
|
||||
|
||||
var tableView: UITableView!
|
||||
var searchBar: UISearchBar!
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private let contactCellReuseIdentifier = "contactCellReuseIdentifier"
|
||||
|
||||
private var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
// HACK: Though we don't have an input accessory view, the VC we are presented above (ConversationVC) does.
|
||||
// If the app is backgrounded and then foregrounded, when OWSWindowManager calls mainWindow.makeKeyAndVisible
|
||||
// the ConversationVC's inputAccessoryView will appear *above* us unless we'd previously become first responder.
|
||||
override public var canBecomeFirstResponder: Bool {
|
||||
Logger.debug("")
|
||||
return true
|
||||
}
|
||||
|
||||
override public func becomeFirstResponder() -> Bool {
|
||||
Logger.debug("")
|
||||
return super.becomeFirstResponder()
|
||||
}
|
||||
|
||||
override public func resignFirstResponder() -> Bool {
|
||||
Logger.debug("")
|
||||
return super.resignFirstResponder()
|
||||
}
|
||||
|
||||
private let collation = UILocalizedIndexedCollation.current()
|
||||
public var collationForTests: UILocalizedIndexedCollation {
|
||||
get {
|
||||
return collation
|
||||
}
|
||||
}
|
||||
private let contactStore = CNContactStore()
|
||||
|
||||
// Data Source State
|
||||
private lazy var sections = [[CNContact]]()
|
||||
private lazy var filteredSections = [[CNContact]]()
|
||||
private lazy var selectedContacts = [Contact]()
|
||||
|
||||
// Configuration
|
||||
@objc
|
||||
public weak var contactsPickerDelegate: ContactsPickerDelegate?
|
||||
private let subtitleCellType: SubtitleCellValue
|
||||
private let allowsMultipleSelection: Bool
|
||||
private let allowedContactKeys: [CNKeyDescriptor] = ContactsFrameworkContactStoreAdaptee.allowedContactKeys
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
@objc
|
||||
required public init(allowsMultipleSelection: Bool, subtitleCellType: SubtitleCellValue) {
|
||||
self.allowsMultipleSelection = allowsMultipleSelection
|
||||
self.subtitleCellType = subtitleCellType
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle Methods
|
||||
|
||||
override public func loadView() {
|
||||
self.view = UIView()
|
||||
let tableView = UITableView()
|
||||
self.tableView = tableView
|
||||
self.tableView.separatorColor = Theme.cellSeparatorColor
|
||||
|
||||
view.addSubview(tableView)
|
||||
tableView.autoPinEdge(toSuperviewEdge: .top)
|
||||
tableView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
tableView.autoPinEdge(toSuperviewSafeArea: .leading)
|
||||
tableView.autoPinEdge(toSuperviewSafeArea: .trailing)
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
|
||||
let searchBar = OWSSearchBar()
|
||||
self.searchBar = searchBar
|
||||
searchBar.delegate = self
|
||||
searchBar.sizeToFit()
|
||||
|
||||
tableView.tableHeaderView = searchBar
|
||||
}
|
||||
|
||||
override open func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
self.view.backgroundColor = Theme.backgroundColor
|
||||
self.tableView.backgroundColor = Theme.backgroundColor
|
||||
|
||||
searchBar.placeholder = NSLocalizedString("INVITE_FRIENDS_PICKER_SEARCHBAR_PLACEHOLDER", comment: "Search")
|
||||
|
||||
// Auto size cells for dynamic type
|
||||
tableView.estimatedRowHeight = 60.0
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 60
|
||||
|
||||
tableView.allowsMultipleSelection = allowsMultipleSelection
|
||||
|
||||
tableView.separatorInset = UIEdgeInsets(top: 0, left: ContactCell.kSeparatorHInset, bottom: 0, right: 16)
|
||||
|
||||
registerContactCell()
|
||||
initializeBarButtons()
|
||||
reloadContacts()
|
||||
updateSearchResults(searchText: "")
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(self.didChangePreferredContentSize), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
public func didChangePreferredContentSize() {
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
|
||||
private func initializeBarButtons() {
|
||||
let cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(onTouchCancelButton))
|
||||
self.navigationItem.leftBarButtonItem = cancelButton
|
||||
|
||||
if allowsMultipleSelection {
|
||||
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(onTouchDoneButton))
|
||||
self.navigationItem.rightBarButtonItem = doneButton
|
||||
}
|
||||
}
|
||||
|
||||
private func registerContactCell() {
|
||||
tableView.register(ContactCell.self, forCellReuseIdentifier: contactCellReuseIdentifier)
|
||||
}
|
||||
|
||||
// MARK: - Contact Operations
|
||||
|
||||
private func reloadContacts() {
|
||||
getContacts( onError: { error in
|
||||
Logger.error("failed to reload contacts with error:\(error)")
|
||||
})
|
||||
}
|
||||
|
||||
private func getContacts(onError errorHandler: @escaping (_ error: Error) -> Void) {
|
||||
switch CNContactStore.authorizationStatus(for: CNEntityType.contacts) {
|
||||
case CNAuthorizationStatus.denied, CNAuthorizationStatus.restricted:
|
||||
let title = NSLocalizedString("INVITE_FLOW_REQUIRES_CONTACT_ACCESS_TITLE", comment: "Alert title when contacts disabled while trying to invite contacts to signal")
|
||||
let body = NSLocalizedString("INVITE_FLOW_REQUIRES_CONTACT_ACCESS_BODY", comment: "Alert body when contacts disabled while trying to invite contacts to signal")
|
||||
|
||||
let alert = UIAlertController(title: title, message: body, preferredStyle: .alert)
|
||||
|
||||
let dismissText = CommonStrings.cancelButton
|
||||
|
||||
let cancelAction = UIAlertAction(title: dismissText, style: .cancel, handler: { _ in
|
||||
let error = NSError(domain: "contactsPickerErrorDomain", code: 1, userInfo: [NSLocalizedDescriptionKey: "No Contacts Access"])
|
||||
self.contactsPickerDelegate?.contactsPicker(self, contactFetchDidFail: error)
|
||||
errorHandler(error)
|
||||
})
|
||||
alert.addAction(cancelAction)
|
||||
|
||||
let settingsText = CommonStrings.openSettingsButton
|
||||
let openSettingsAction = UIAlertAction(title: settingsText, style: .default, handler: { (_) in
|
||||
UIApplication.shared.openSystemSettings()
|
||||
})
|
||||
alert.addAction(openSettingsAction)
|
||||
|
||||
self.presentAlert(alert)
|
||||
|
||||
case CNAuthorizationStatus.notDetermined:
|
||||
//This case means the user is prompted for the first time for allowing contacts
|
||||
contactStore.requestAccess(for: CNEntityType.contacts) { (granted, error) -> Void in
|
||||
//At this point an alert is provided to the user to provide access to contacts. This will get invoked if a user responds to the alert
|
||||
if granted {
|
||||
self.getContacts(onError: errorHandler)
|
||||
} else {
|
||||
errorHandler(error!)
|
||||
}
|
||||
}
|
||||
|
||||
case CNAuthorizationStatus.authorized:
|
||||
//Authorization granted by user for this app.
|
||||
var contacts = [CNContact]()
|
||||
|
||||
do {
|
||||
let contactFetchRequest = CNContactFetchRequest(keysToFetch: allowedContactKeys)
|
||||
contactFetchRequest.sortOrder = .userDefault
|
||||
try contactStore.enumerateContacts(with: contactFetchRequest) { (contact, _) -> Void in
|
||||
contacts.append(contact)
|
||||
}
|
||||
self.sections = collatedContacts(contacts)
|
||||
} catch let error as NSError {
|
||||
Logger.error("Failed to fetch contacts with error:\(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func collatedContacts(_ contacts: [CNContact]) -> [[CNContact]] {
|
||||
let selector: Selector = #selector(getter: CNContact.nameForCollating)
|
||||
|
||||
var collated = Array(repeating: [CNContact](), count: collation.sectionTitles.count)
|
||||
for contact in contacts {
|
||||
let sectionNumber = collation.section(for: contact, collationStringSelector: selector)
|
||||
collated[sectionNumber].append(contact)
|
||||
}
|
||||
return collated
|
||||
}
|
||||
|
||||
// MARK: - Table View DataSource
|
||||
|
||||
open func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return self.collation.sectionTitles.count
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let dataSource = filteredSections
|
||||
|
||||
guard section < dataSource.count else {
|
||||
return 0
|
||||
}
|
||||
|
||||
return dataSource[section].count
|
||||
}
|
||||
|
||||
// MARK: - Table View Delegates
|
||||
|
||||
open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: contactCellReuseIdentifier, for: indexPath) as? ContactCell else {
|
||||
owsFailDebug("cell had unexpected type")
|
||||
return UITableViewCell()
|
||||
}
|
||||
|
||||
let dataSource = filteredSections
|
||||
let cnContact = dataSource[indexPath.section][indexPath.row]
|
||||
let contact = Contact(systemContact: cnContact)
|
||||
|
||||
cell.configure(contact: contact, subtitleType: subtitleCellType, showsWhenSelected: self.allowsMultipleSelection, contactsManager: self.contactsManager)
|
||||
let isSelected = selectedContacts.contains(where: { $0.uniqueId == contact.uniqueId })
|
||||
cell.isSelected = isSelected
|
||||
|
||||
// Make sure we preserve selection across tableView.reloadData which happens when toggling between
|
||||
// search controller
|
||||
if (isSelected) {
|
||||
self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
|
||||
} else {
|
||||
self.tableView.deselectRow(at: indexPath, animated: false)
|
||||
}
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
|
||||
let cell = tableView.cellForRow(at: indexPath) as! ContactCell
|
||||
let deselectedContact = cell.contact!
|
||||
|
||||
selectedContacts = selectedContacts.filter {
|
||||
return $0.uniqueId != deselectedContact.uniqueId
|
||||
}
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
Logger.verbose("")
|
||||
|
||||
let cell = tableView.cellForRow(at: indexPath) as! ContactCell
|
||||
let selectedContact = cell.contact!
|
||||
|
||||
guard (contactsPickerDelegate == nil || contactsPickerDelegate!.contactsPicker(self, shouldSelectContact: selectedContact)) else {
|
||||
self.tableView.deselectRow(at: indexPath, animated: false)
|
||||
return
|
||||
}
|
||||
|
||||
selectedContacts.append(selectedContact)
|
||||
|
||||
if !allowsMultipleSelection {
|
||||
// Single selection code
|
||||
self.contactsPickerDelegate?.contactsPicker(self, didSelectContact: selectedContact)
|
||||
}
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int {
|
||||
return collation.section(forSectionIndexTitle: index)
|
||||
}
|
||||
|
||||
open func sectionIndexTitles(for tableView: UITableView) -> [String]? {
|
||||
return collation.sectionIndexTitles
|
||||
}
|
||||
|
||||
open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
let dataSource = filteredSections
|
||||
|
||||
guard section < dataSource.count else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Don't show empty sections
|
||||
if dataSource[section].count > 0 {
|
||||
guard section < collation.sectionTitles.count else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return collation.sectionTitles[section]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Button Actions
|
||||
|
||||
@objc func onTouchCancelButton() {
|
||||
contactsPickerDelegate?.contactsPickerDidCancel(self)
|
||||
}
|
||||
|
||||
@objc func onTouchDoneButton() {
|
||||
contactsPickerDelegate?.contactsPicker(self, didSelectMultipleContacts: selectedContacts)
|
||||
}
|
||||
|
||||
// MARK: - Search Actions
|
||||
open func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
updateSearchResults(searchText: searchText)
|
||||
}
|
||||
|
||||
open func updateSearchResults(searchText: String) {
|
||||
let predicate: NSPredicate
|
||||
if searchText.isEmpty {
|
||||
filteredSections = sections
|
||||
} else {
|
||||
do {
|
||||
predicate = CNContact.predicateForContacts(matchingName: searchText)
|
||||
let filteredContacts = try contactStore.unifiedContacts(matching: predicate, keysToFetch: allowedContactKeys)
|
||||
filteredSections = collatedContacts(filteredContacts)
|
||||
} catch let error as NSError {
|
||||
Logger.error("updating search results failed with error: \(error)")
|
||||
}
|
||||
}
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
let ContactSortOrder = computeSortOrder()
|
||||
|
||||
func computeSortOrder() -> CNContactSortOrder {
|
||||
let comparator = CNContact.comparator(forNameSortOrder: .userDefault)
|
||||
|
||||
let contact0 = CNMutableContact()
|
||||
contact0.givenName = "A"
|
||||
contact0.familyName = "Z"
|
||||
|
||||
let contact1 = CNMutableContact()
|
||||
contact1.givenName = "Z"
|
||||
contact1.familyName = "A"
|
||||
|
||||
let result = comparator(contact0, contact1)
|
||||
|
||||
if result == .orderedAscending {
|
||||
return .givenName
|
||||
} else {
|
||||
return .familyName
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension CNContact {
|
||||
/**
|
||||
* Sorting Key used by collation
|
||||
*/
|
||||
@objc var nameForCollating: String {
|
||||
get {
|
||||
if self.familyName.isEmpty && self.givenName.isEmpty {
|
||||
return self.emailAddresses.first?.value as String? ?? ""
|
||||
}
|
||||
|
||||
let compositeName: String
|
||||
if ContactSortOrder == .familyName {
|
||||
compositeName = "\(self.familyName) \(self.givenName)"
|
||||
} else {
|
||||
compositeName = "\(self.givenName) \(self.familyName)"
|
||||
}
|
||||
return compositeName.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc
|
||||
class ConversationConfigurationSyncOperation: OWSOperation {
|
||||
|
||||
enum ColorSyncOperationError: Error {
|
||||
case assertionError(description: String)
|
||||
}
|
||||
|
||||
private var dbConnection: YapDatabaseConnection {
|
||||
return OWSPrimaryStorage.shared().dbReadConnection
|
||||
}
|
||||
|
||||
private var messageSenderJobQueue: MessageSenderJobQueue {
|
||||
return SSKEnvironment.shared.messageSenderJobQueue
|
||||
}
|
||||
|
||||
private var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
private var syncManager: OWSSyncManagerProtocol {
|
||||
return SSKEnvironment.shared.syncManager
|
||||
}
|
||||
|
||||
private let thread: TSThread
|
||||
|
||||
@objc
|
||||
public init(thread: TSThread) {
|
||||
self.thread = thread
|
||||
super.init()
|
||||
}
|
||||
|
||||
override public func run() {
|
||||
if let contactThread = thread as? TSContactThread {
|
||||
sync(contactThread: contactThread)
|
||||
} else if let groupThread = thread as? TSGroupThread {
|
||||
sync(groupThread: groupThread)
|
||||
} else {
|
||||
self.reportAssertionError(description: "unknown thread type")
|
||||
}
|
||||
}
|
||||
|
||||
private func reportAssertionError(description: String) {
|
||||
let error = ColorSyncOperationError.assertionError(description: description)
|
||||
self.reportError(error)
|
||||
}
|
||||
|
||||
private func sync(contactThread: TSContactThread) {
|
||||
guard let signalAccount: SignalAccount = self.contactsManager.fetchSignalAccount(forRecipientId: contactThread.contactIdentifier()) else {
|
||||
reportAssertionError(description: "unable to find signalAccount")
|
||||
return
|
||||
}
|
||||
|
||||
syncManager.syncContacts(for: [signalAccount]).retainUntilComplete()
|
||||
}
|
||||
|
||||
private func sync(groupThread: TSGroupThread) {
|
||||
// TODO sync only the affected group
|
||||
// The current implementation works, but seems wasteful.
|
||||
// Does desktop handle single group sync correctly?
|
||||
// What does Android do?
|
||||
let syncMessage: OWSSyncGroupsMessage = OWSSyncGroupsMessage(groupThread: groupThread)
|
||||
|
||||
var dataSource: DataSource?
|
||||
self.dbConnection.read { transaction in
|
||||
guard let messageData: Data = syncMessage.buildPlainTextAttachmentData(with: transaction) else {
|
||||
owsFailDebug("could not serialize sync groups data")
|
||||
return
|
||||
}
|
||||
dataSource = DataSourceValue.dataSource(withSyncMessageData: messageData)
|
||||
}
|
||||
|
||||
guard let attachmentDataSource = dataSource else {
|
||||
self.reportAssertionError(description: "unable to build attachment data source")
|
||||
return
|
||||
}
|
||||
|
||||
self.sendConfiguration(attachmentDataSource: attachmentDataSource, syncMessage: syncMessage)
|
||||
}
|
||||
|
||||
private func sendConfiguration(attachmentDataSource: DataSource, syncMessage: OWSOutgoingSyncMessage) {
|
||||
self.messageSenderJobQueue.add(mediaMessage: syncMessage,
|
||||
dataSource: attachmentDataSource,
|
||||
contentType: OWSMimeTypeApplicationOctetStream,
|
||||
sourceFilename: nil,
|
||||
caption: nil,
|
||||
albumMessageId: nil,
|
||||
isTemporaryAttachment: true)
|
||||
self.reportSuccess()
|
||||
}
|
||||
|
||||
}
|
|
@ -6,10 +6,10 @@
|
|||
#import "OWSBezierPathView.h"
|
||||
#import "OWSProgressView.h"
|
||||
#import <SignalUtilitiesKit/UIFont+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SignalUtilitiesKit/AppContext.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/AppContext.h>
|
||||
#import <SignalUtilitiesKit/OWSUploadOperation.h>
|
||||
#import <SignalUtilitiesKit/TSAttachmentStream.h>
|
||||
#import <SessionMessagingKit/TSAttachmentStream.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -13,12 +13,6 @@ public class ConversationMediaView: UIView {
|
|||
case failed
|
||||
}
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private var attachmentDownloads: OWSAttachmentDownloads {
|
||||
return SSKEnvironment.shared.attachmentDownloads
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private let mediaCache: NSCache<NSString, AnyObject>
|
||||
|
@ -163,11 +157,13 @@ public class ConversationMediaView: UIView {
|
|||
configure(forError: .invalid)
|
||||
return
|
||||
}
|
||||
/*
|
||||
guard nil != attachmentDownloads.downloadProgress(forAttachmentId: attachmentId) else {
|
||||
// Not being downloaded.
|
||||
configure(forError: .missing)
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
||||
backgroundColor = (Theme.isDarkThemeEnabled ? .ows_gray90 : .ows_gray05)
|
||||
let view: UIView
|
||||
|
|
|
@ -35,23 +35,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
#pragma mark - System Cell
|
||||
|
||||
- (void)tappedNonBlockingIdentityChangeForRecipientId:(nullable NSString *)signalId;
|
||||
- (void)tappedInvalidIdentityKeyErrorMessage:(TSInvalidIdentityKeyErrorMessage *)errorMessage;
|
||||
- (void)tappedCorruptedMessage:(TSErrorMessage *)message;
|
||||
- (void)resendGroupUpdateForErrorMessage:(TSErrorMessage *)message;
|
||||
- (void)showFingerprintWithRecipientId:(NSString *)recipientId;
|
||||
- (void)showConversationSettings;
|
||||
- (void)handleCallTap:(TSCall *)call;
|
||||
|
||||
#pragma mark - Offers
|
||||
|
||||
- (void)tappedUnknownContactBlockOfferMessage:(OWSContactOffersInteraction *)interaction;
|
||||
- (void)tappedAddToContactsOfferMessage:(OWSContactOffersInteraction *)interaction;
|
||||
- (void)tappedAddToProfileWhitelistOfferMessage:(OWSContactOffersInteraction *)interaction;
|
||||
|
||||
#pragma mark - Formatting
|
||||
|
||||
- (NSAttributedString *)attributedContactOrProfileNameForPhoneIdentifier:(NSString *)recipientId;
|
||||
|
||||
#pragma mark - Caching
|
||||
|
||||
|
@ -61,10 +47,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (void)didTapFailedOutgoingMessage:(TSOutgoingMessage *)message;
|
||||
|
||||
#pragma mark - Contacts
|
||||
|
||||
- (OWSContactsManager *)contactsManager;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
|
|
@ -7,12 +7,6 @@ import Foundation
|
|||
@objc
|
||||
public class MediaDownloadView: UIView {
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private var attachmentDownloads: OWSAttachmentDownloads {
|
||||
return SSKEnvironment.shared.attachmentDownloads
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private let attachmentId: String
|
||||
|
@ -75,13 +69,13 @@ public class MediaDownloadView: UIView {
|
|||
shapeLayer1.frame = self.bounds
|
||||
shapeLayer2.frame = self.bounds
|
||||
|
||||
guard let progress = attachmentDownloads.downloadProgress(forAttachmentId: attachmentId) else {
|
||||
Logger.warn("No progress for attachment.")
|
||||
shapeLayer1.path = nil
|
||||
shapeLayer2.path = nil
|
||||
return
|
||||
}
|
||||
shapeLayer1.path = nil
|
||||
shapeLayer2.path = nil
|
||||
return
|
||||
|
||||
// We can't display download progress yet
|
||||
|
||||
/*
|
||||
// Prevent the shape layer from animating changes.
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
|
@ -115,5 +109,6 @@ public class MediaDownloadView: UIView {
|
|||
shapeLayer2.fillColor = fillColor2.cgColor
|
||||
|
||||
CATransaction.commit()
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
#import "OWSBubbleShapeView.h"
|
||||
#import "OWSBubbleView.h"
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
#import "OWSBubbleView.h"
|
||||
#import "MainAppContext.h"
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
#import "Session-Swift.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConversationViewCell.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class OWSContactOffersInteraction;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSContactOffersCell : ConversationViewCell
|
||||
|
||||
+ (NSString *)cellReuseIdentifier;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,263 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSContactOffersCell.h"
|
||||
#import "ConversationViewItem.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalUtilitiesKit/OWSContactOffersInteraction.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIFont+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSContactOffersCell ()
|
||||
|
||||
@property (nonatomic) UILabel *titleLabel;
|
||||
@property (nonatomic) UIButton *addToContactsButton;
|
||||
@property (nonatomic) UIButton *addToProfileWhitelistButton;
|
||||
@property (nonatomic) UIButton *blockButton;
|
||||
@property (nonatomic) NSArray<NSLayoutConstraint *> *layoutConstraints;
|
||||
@property (nonatomic) UIStackView *stackView;
|
||||
@property (nonatomic) UIStackView *buttonStackView;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSContactOffersCell
|
||||
|
||||
// `[UIView init]` invokes `[self initWithFrame:...]`.
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
[self commontInit];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)commontInit
|
||||
{
|
||||
OWSAssertDebug(!self.titleLabel);
|
||||
|
||||
self.layoutMargins = UIEdgeInsetsZero;
|
||||
self.contentView.layoutMargins = UIEdgeInsetsZero;
|
||||
self.layoutConstraints = @[];
|
||||
|
||||
self.titleLabel = [UILabel new];
|
||||
self.titleLabel.text = NSLocalizedString(@"CONVERSATION_VIEW_CONTACTS_OFFER_TITLE",
|
||||
@"Title for the group of buttons show for unknown contacts offering to add them to contacts, etc.");
|
||||
self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
self.titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
|
||||
self.addToContactsButton = [self
|
||||
createButtonWithTitle:
|
||||
NSLocalizedString(@"CONVERSATION_VIEW_ADD_TO_CONTACTS_OFFER",
|
||||
@"Message shown in conversation view that offers to add an unknown user to your phone's contacts.")
|
||||
selector:@selector(addToContacts)];
|
||||
self.addToProfileWhitelistButton = [self
|
||||
createButtonWithTitle:NSLocalizedString(@"CONVERSATION_VIEW_ADD_USER_TO_PROFILE_WHITELIST_OFFER",
|
||||
@"Message shown in conversation view that offers to share your profile with a user.")
|
||||
selector:@selector(addToProfileWhitelist)];
|
||||
self.blockButton =
|
||||
[self createButtonWithTitle:NSLocalizedString(@"CONVERSATION_VIEW_UNKNOWN_CONTACT_BLOCK_OFFER",
|
||||
@"Message shown in conversation view that offers to block an unknown user.")
|
||||
selector:@selector(block)];
|
||||
|
||||
UIStackView *buttonStackView = [[UIStackView alloc] initWithArrangedSubviews:self.buttons];
|
||||
buttonStackView.axis = UILayoutConstraintAxisVertical;
|
||||
buttonStackView.spacing = self.vSpacing;
|
||||
self.buttonStackView = buttonStackView;
|
||||
|
||||
self.stackView = [[UIStackView alloc] initWithArrangedSubviews:@[
|
||||
self.titleLabel,
|
||||
buttonStackView,
|
||||
]];
|
||||
self.stackView.axis = UILayoutConstraintAxisVertical;
|
||||
self.stackView.spacing = self.vSpacing;
|
||||
self.stackView.alignment = UIStackViewAlignmentCenter;
|
||||
[self.contentView addSubview:self.stackView];
|
||||
}
|
||||
|
||||
- (void)configureFonts
|
||||
{
|
||||
self.titleLabel.font = UIFont.ows_dynamicTypeSubheadlineFont;
|
||||
|
||||
UIFont *buttonFont = UIFont.ows_dynamicTypeSubheadlineFont.ows_mediumWeight;
|
||||
self.addToContactsButton.titleLabel.font = buttonFont;
|
||||
self.addToProfileWhitelistButton.titleLabel.font = buttonFont;
|
||||
self.blockButton.titleLabel.font = buttonFont;
|
||||
}
|
||||
|
||||
- (UIButton *)createButtonWithTitle:(NSString *)title selector:(SEL)selector
|
||||
{
|
||||
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[button setTitle:title forState:UIControlStateNormal];
|
||||
button.titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
button.layer.cornerRadius = 4.f;
|
||||
[button addTarget:self action:selector forControlEvents:UIControlEventTouchUpInside];
|
||||
button.contentEdgeInsets = UIEdgeInsetsMake(0, 10.f, 0, 10.f);
|
||||
return button;
|
||||
}
|
||||
|
||||
+ (NSString *)cellReuseIdentifier
|
||||
{
|
||||
return NSStringFromClass([self class]);
|
||||
}
|
||||
|
||||
- (void)loadForDisplay
|
||||
{
|
||||
OWSAssertDebug(self.conversationStyle);
|
||||
OWSAssertDebug(self.conversationStyle.viewWidth > 0);
|
||||
OWSAssertDebug(self.viewItem);
|
||||
OWSAssertDebug([self.viewItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]);
|
||||
|
||||
self.backgroundColor = [Theme backgroundColor];
|
||||
|
||||
[self configureFonts];
|
||||
|
||||
self.titleLabel.textColor = Theme.secondaryColor;
|
||||
for (UIButton *button in self.buttons) {
|
||||
[button setTitleColor:[UIColor ows_signalBlueColor] forState:UIControlStateNormal];
|
||||
[button setBackgroundColor:Theme.conversationButtonBackgroundColor];
|
||||
}
|
||||
|
||||
OWSContactOffersInteraction *interaction = (OWSContactOffersInteraction *)self.viewItem.interaction;
|
||||
|
||||
OWSAssertDebug(
|
||||
interaction.hasBlockOffer || interaction.hasAddToContactsOffer || interaction.hasAddToProfileWhitelistOffer);
|
||||
|
||||
self.addToContactsButton.hidden = !interaction.hasAddToContactsOffer;
|
||||
self.addToProfileWhitelistButton.hidden = !interaction.hasAddToProfileWhitelistOffer;
|
||||
self.blockButton.hidden = !interaction.hasBlockOffer;
|
||||
|
||||
[NSLayoutConstraint deactivateConstraints:self.layoutConstraints];
|
||||
self.layoutConstraints = @[
|
||||
[self.addToContactsButton autoSetDimension:ALDimensionHeight toSize:self.buttonHeight],
|
||||
[self.addToProfileWhitelistButton autoSetDimension:ALDimensionHeight toSize:self.buttonHeight],
|
||||
[self.blockButton autoSetDimension:ALDimensionHeight toSize:self.buttonHeight],
|
||||
|
||||
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.topVMargin],
|
||||
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.bottomVMargin],
|
||||
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeLeading
|
||||
withInset:self.conversationStyle.fullWidthGutterLeading],
|
||||
[self.stackView autoPinEdgeToSuperviewEdge:ALEdgeTrailing
|
||||
withInset:self.conversationStyle.fullWidthGutterTrailing],
|
||||
];
|
||||
|
||||
// This hack fixes a bug that I don't understand.
|
||||
//
|
||||
// On an iPhone 5C running iOS 10.3.3,
|
||||
//
|
||||
// * Alice is a contact for which we should show some but not all contact offer buttons.
|
||||
// * Delete thread with Alice.
|
||||
// * Send yourself a message from Alice.
|
||||
// * Open conversation with Alice.
|
||||
//
|
||||
// Expected: Some (but not all) offer buttons are displayed.
|
||||
// Observed: All offer buttons are displayed, in a cramped layout.
|
||||
for (UIButton *button in self.buttons) {
|
||||
[button removeFromSuperview];
|
||||
}
|
||||
for (UIButton *button in self.buttons) {
|
||||
if (!button.hidden) {
|
||||
[self.buttonStackView addArrangedSubview:button];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<UIButton *> *)buttons
|
||||
{
|
||||
return @[
|
||||
self.addToContactsButton,
|
||||
self.addToProfileWhitelistButton,
|
||||
self.blockButton,
|
||||
];
|
||||
}
|
||||
|
||||
- (CGFloat)topVMargin
|
||||
{
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
- (CGFloat)bottomVMargin
|
||||
{
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
- (CGFloat)vSpacing
|
||||
{
|
||||
return 8.f;
|
||||
}
|
||||
|
||||
- (CGFloat)buttonHeight
|
||||
{
|
||||
return (24.f + self.addToContactsButton.titleLabel.font.lineHeight);
|
||||
}
|
||||
|
||||
- (CGSize)cellSize
|
||||
{
|
||||
OWSAssertDebug(self.conversationStyle);
|
||||
OWSAssertDebug(self.conversationStyle.viewWidth > 0);
|
||||
OWSAssertDebug(self.viewItem);
|
||||
OWSAssertDebug([self.viewItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]);
|
||||
|
||||
[self configureFonts];
|
||||
|
||||
OWSContactOffersInteraction *interaction = (OWSContactOffersInteraction *)self.viewItem.interaction;
|
||||
|
||||
CGSize result = CGSizeMake(self.conversationStyle.viewWidth, 0);
|
||||
result.height += self.topVMargin;
|
||||
result.height += self.bottomVMargin;
|
||||
|
||||
result.height += ceil([self.titleLabel sizeThatFits:CGSizeZero].height);
|
||||
|
||||
int buttonCount = ((interaction.hasBlockOffer ? 1 : 0) + (interaction.hasAddToContactsOffer ? 1 : 0)
|
||||
+ (interaction.hasAddToProfileWhitelistOffer ? 1 : 0));
|
||||
result.height += buttonCount * (self.vSpacing + self.buttonHeight);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#pragma mark - Events
|
||||
|
||||
- (nullable OWSContactOffersInteraction *)interaction
|
||||
{
|
||||
OWSAssertDebug(self.viewItem);
|
||||
OWSAssertDebug(self.viewItem.interaction);
|
||||
if (![self.viewItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]) {
|
||||
OWSFailDebug(@"expected OWSContactOffersInteraction but found: %@", self.viewItem.interaction);
|
||||
return nil;
|
||||
}
|
||||
return (OWSContactOffersInteraction *)self.viewItem.interaction;
|
||||
}
|
||||
|
||||
- (void)addToContacts
|
||||
{
|
||||
OWSAssertDebug(self.delegate);
|
||||
OWSAssertDebug(self.interaction);
|
||||
|
||||
[self.delegate tappedAddToContactsOfferMessage:self.interaction];
|
||||
}
|
||||
|
||||
- (void)addToProfileWhitelist
|
||||
{
|
||||
OWSAssertDebug(self.delegate);
|
||||
OWSAssertDebug(self.interaction);
|
||||
|
||||
[self.delegate tappedAddToProfileWhitelistOfferMessage:self.interaction];
|
||||
}
|
||||
|
||||
- (void)block
|
||||
{
|
||||
OWSAssertDebug(self.delegate);
|
||||
OWSAssertDebug(self.interaction);
|
||||
|
||||
[self.delegate tappedUnknownContactBlockOfferMessage:self.interaction];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,33 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ContactShareViewModel;
|
||||
|
||||
@protocol OWSContactShareButtonsViewDelegate <NSObject>
|
||||
|
||||
- (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare;
|
||||
- (void)didTapSendInviteToContactShare:(ContactShareViewModel *)contactShare;
|
||||
- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSContactShareButtonsView : UIView
|
||||
|
||||
- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare
|
||||
delegate:(id<OWSContactShareButtonsViewDelegate>)delegate;
|
||||
|
||||
+ (CGFloat)bubbleHeight;
|
||||
|
||||
// Returns YES IFF the tap was handled.
|
||||
- (BOOL)handleTapGesture:(UITapGestureRecognizer *)sender;
|
||||
|
||||
+ (BOOL)hasAnyButton:(ContactShareViewModel *)contactShare;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,169 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSContactShareButtonsView.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSContact.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSContactShareButtonsView ()
|
||||
|
||||
@property (nonatomic, readonly) ContactShareViewModel *contactShare;
|
||||
@property (nonatomic, weak) id<OWSContactShareButtonsViewDelegate> delegate;
|
||||
|
||||
@property (nonatomic, readonly) OWSContactsManager *contactsManager;
|
||||
|
||||
@property (nonatomic, nullable) UIView *buttonView;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSContactShareButtonsView
|
||||
|
||||
- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare
|
||||
delegate:(id<OWSContactShareButtonsViewDelegate>)delegate
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_delegate = delegate;
|
||||
_contactShare = contactShare;
|
||||
_contactsManager = Environment.shared.contactsManager;
|
||||
|
||||
[self createContents];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
+ (BOOL)hasSendTextButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager
|
||||
{
|
||||
OWSAssertDebug(contactShare);
|
||||
OWSAssertDebug(contactsManager);
|
||||
|
||||
return [contactShare systemContactsWithSignalAccountPhoneNumbers:contactsManager].count > 0;
|
||||
}
|
||||
|
||||
+ (BOOL)hasInviteButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager
|
||||
{
|
||||
OWSAssertDebug(contactShare);
|
||||
OWSAssertDebug(contactsManager);
|
||||
|
||||
return [contactShare systemContactPhoneNumbers:contactsManager].count > 0;
|
||||
}
|
||||
|
||||
+ (BOOL)hasAddToContactsButton:(ContactShareViewModel *)contactShare
|
||||
{
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
return [contactShare e164PhoneNumbers].count > 0;
|
||||
}
|
||||
|
||||
+ (BOOL)hasAnyButton:(ContactShareViewModel *)contactShare
|
||||
{
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
OWSContactsManager *contactsManager = Environment.shared.contactsManager;
|
||||
|
||||
return [self hasAnyButton:contactShare contactsManager:contactsManager];
|
||||
}
|
||||
|
||||
+ (BOOL)hasAnyButton:(ContactShareViewModel *)contactShare contactsManager:(OWSContactsManager *)contactsManager
|
||||
{
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
return ([self hasSendTextButton:contactShare contactsManager:contactsManager] ||
|
||||
[self hasInviteButton:contactShare contactsManager:contactsManager] ||
|
||||
[self hasAddToContactsButton:contactShare]);
|
||||
}
|
||||
|
||||
+ (CGFloat)bubbleHeight
|
||||
{
|
||||
return self.buttonHeight;
|
||||
}
|
||||
|
||||
+ (CGFloat)buttonHeight
|
||||
{
|
||||
return MAX(44.f, self.buttonFont.lineHeight + self.buttonVMargin * 2);
|
||||
}
|
||||
|
||||
+ (UIFont *)buttonFont
|
||||
{
|
||||
return [UIFont ows_dynamicTypeBodyFont].ows_mediumWeight;
|
||||
}
|
||||
|
||||
+ (CGFloat)buttonVMargin
|
||||
{
|
||||
return 5;
|
||||
}
|
||||
|
||||
- (void)createContents
|
||||
{
|
||||
OWSAssertDebug([OWSContactShareButtonsView hasAnyButton:self.contactShare contactsManager:self.contactsManager]);
|
||||
|
||||
self.layoutMargins = UIEdgeInsetsZero;
|
||||
self.backgroundColor = Theme.conversationButtonBackgroundColor;
|
||||
|
||||
UILabel *label = [UILabel new];
|
||||
self.buttonView = label;
|
||||
if ([OWSContactShareButtonsView hasSendTextButton:self.contactShare contactsManager:self.contactsManager]) {
|
||||
label.text
|
||||
= NSLocalizedString(@"ACTION_SEND_MESSAGE", @"Label for button that lets you send a message to a contact.");
|
||||
} else if ([OWSContactShareButtonsView hasInviteButton:self.contactShare contactsManager:self.contactsManager]) {
|
||||
label.text = NSLocalizedString(@"ACTION_INVITE", @"Label for 'invite' button in contact view.");
|
||||
} else if ([OWSContactShareButtonsView hasAddToContactsButton:self.contactShare]) {
|
||||
label.text = NSLocalizedString(@"CONVERSATION_VIEW_ADD_TO_CONTACTS_OFFER",
|
||||
@"Message shown in conversation view that offers to add an unknown user to your phone's contacts.");
|
||||
} else {
|
||||
OWSFailDebug(@"unexpected button state.");
|
||||
}
|
||||
label.font = OWSContactShareButtonsView.buttonFont;
|
||||
label.textColor = (Theme.isDarkThemeEnabled ? UIColor.ows_whiteColor : UIColor.ows_materialBlueColor);
|
||||
label.textAlignment = NSTextAlignmentCenter;
|
||||
[self addSubview:label];
|
||||
[label ows_autoPinToSuperviewEdges];
|
||||
[label autoSetDimension:ALDimensionHeight toSize:OWSContactShareButtonsView.buttonHeight];
|
||||
|
||||
self.userInteractionEnabled = YES;
|
||||
UITapGestureRecognizer *tap =
|
||||
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
|
||||
[self addGestureRecognizer:tap];
|
||||
}
|
||||
|
||||
- (BOOL)handleTapGesture:(UITapGestureRecognizer *)sender
|
||||
{
|
||||
if (!self.buttonView) {
|
||||
return NO;
|
||||
}
|
||||
CGPoint location = [sender locationInView:self.buttonView];
|
||||
if (!CGRectContainsPoint(self.buttonView.bounds, location)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if ([OWSContactShareButtonsView hasSendTextButton:self.contactShare contactsManager:self.contactsManager]) {
|
||||
[self.delegate didTapSendMessageToContactShare:self.contactShare];
|
||||
} else if ([OWSContactShareButtonsView hasInviteButton:self.contactShare contactsManager:self.contactsManager]) {
|
||||
[self.delegate didTapSendInviteToContactShare:self.contactShare];
|
||||
} else if ([OWSContactShareButtonsView hasAddToContactsButton:self.contactShare]) {
|
||||
[self.delegate didTapShowAddToContactUIForContactShare:self.contactShare];
|
||||
} else {
|
||||
OWSFailDebug(@"unexpected button tap.");
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,22 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class ContactShareViewModel;
|
||||
@class ConversationStyle;
|
||||
|
||||
@interface OWSContactShareView : UIView
|
||||
|
||||
- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare
|
||||
isIncoming:(BOOL)isIncoming
|
||||
conversationStyle:(ConversationStyle *)conversationStyle;
|
||||
|
||||
- (void)createContents;
|
||||
|
||||
+ (CGFloat)bubbleHeight;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,165 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSContactShareView.h"
|
||||
#import "OWSContactAvatarBuilder.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSContact.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSContactShareView ()
|
||||
|
||||
@property (nonatomic, readonly) ContactShareViewModel *contactShare;
|
||||
|
||||
@property (nonatomic, readonly) BOOL isIncoming;
|
||||
@property (nonatomic, readonly) ConversationStyle *conversationStyle;
|
||||
@property (nonatomic, readonly) OWSContactsManager *contactsManager;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSContactShareView
|
||||
|
||||
- (instancetype)initWithContactShare:(ContactShareViewModel *)contactShare
|
||||
isIncoming:(BOOL)isIncoming
|
||||
conversationStyle:(ConversationStyle *)conversationStyle
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_contactShare = contactShare;
|
||||
_isIncoming = isIncoming;
|
||||
_conversationStyle = conversationStyle;
|
||||
_contactsManager = Environment.shared.contactsManager;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (CGFloat)hMargin
|
||||
{
|
||||
return 12.f;
|
||||
}
|
||||
|
||||
+ (CGFloat)vMargin
|
||||
{
|
||||
return 0.f;
|
||||
}
|
||||
|
||||
- (CGFloat)iconHSpacing
|
||||
{
|
||||
return 8.f;
|
||||
}
|
||||
|
||||
+ (CGFloat)bubbleHeight
|
||||
{
|
||||
return self.contentHeight;
|
||||
}
|
||||
|
||||
+ (CGFloat)contentHeight
|
||||
{
|
||||
CGFloat labelsHeight = (self.nameFont.lineHeight + self.labelsVSpacing + self.subtitleFont.lineHeight);
|
||||
CGFloat contentHeight = MAX(self.iconSize, labelsHeight);
|
||||
contentHeight += OWSContactShareView.vMargin * 2;
|
||||
return contentHeight;
|
||||
}
|
||||
|
||||
+ (CGFloat)iconSize
|
||||
{
|
||||
return kStandardAvatarSize;
|
||||
}
|
||||
|
||||
- (CGFloat)iconSize
|
||||
{
|
||||
return [OWSContactShareView iconSize];
|
||||
}
|
||||
|
||||
+ (UIFont *)nameFont
|
||||
{
|
||||
return [UIFont ows_dynamicTypeBodyFont];
|
||||
}
|
||||
|
||||
+ (UIFont *)subtitleFont
|
||||
{
|
||||
return [UIFont ows_dynamicTypeCaption1Font];
|
||||
}
|
||||
|
||||
+ (CGFloat)labelsVSpacing
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
- (void)createContents
|
||||
{
|
||||
self.layoutMargins = UIEdgeInsetsZero;
|
||||
|
||||
UIColor *textColor = [self.conversationStyle bubbleTextColorWithIsIncoming:self.isIncoming];
|
||||
|
||||
AvatarImageView *avatarView = [AvatarImageView new];
|
||||
avatarView.image =
|
||||
[self.contactShare getAvatarImageWithDiameter:self.iconSize contactsManager:self.contactsManager];
|
||||
|
||||
[avatarView autoSetDimension:ALDimensionWidth toSize:self.iconSize];
|
||||
[avatarView autoSetDimension:ALDimensionHeight toSize:self.iconSize];
|
||||
[avatarView setCompressionResistanceHigh];
|
||||
[avatarView setContentHuggingHigh];
|
||||
|
||||
UILabel *topLabel = [UILabel new];
|
||||
topLabel.text = self.contactShare.displayName;
|
||||
topLabel.textColor = textColor;
|
||||
topLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
topLabel.font = OWSContactShareView.nameFont;
|
||||
|
||||
UIStackView *labelsView = [UIStackView new];
|
||||
labelsView.axis = UILayoutConstraintAxisVertical;
|
||||
labelsView.spacing = OWSContactShareView.labelsVSpacing;
|
||||
[labelsView addArrangedSubview:topLabel];
|
||||
|
||||
NSString *_Nullable firstPhoneNumber =
|
||||
[self.contactShare systemContactsWithSignalAccountPhoneNumbers:self.contactsManager].firstObject;
|
||||
if (firstPhoneNumber.length > 0) {
|
||||
UILabel *bottomLabel = [UILabel new];
|
||||
bottomLabel.text = [PhoneNumber bestEffortLocalizedPhoneNumberWithE164:firstPhoneNumber];
|
||||
bottomLabel.textColor = [self.conversationStyle bubbleSecondaryTextColorWithIsIncoming:self.isIncoming];
|
||||
bottomLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
bottomLabel.font = OWSContactShareView.subtitleFont;
|
||||
[labelsView addArrangedSubview:bottomLabel];
|
||||
}
|
||||
|
||||
UIImage *disclosureImage =
|
||||
[UIImage imageNamed:(CurrentAppContext().isRTL ? @"small_chevron_left" : @"small_chevron_right")];
|
||||
OWSAssertDebug(disclosureImage);
|
||||
UIImageView *disclosureImageView = [UIImageView new];
|
||||
disclosureImageView.image = [disclosureImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
disclosureImageView.tintColor = textColor;
|
||||
[disclosureImageView setCompressionResistanceHigh];
|
||||
[disclosureImageView setContentHuggingHigh];
|
||||
|
||||
UIStackView *hStackView = [UIStackView new];
|
||||
hStackView.axis = UILayoutConstraintAxisHorizontal;
|
||||
hStackView.spacing = self.iconHSpacing;
|
||||
hStackView.alignment = UIStackViewAlignmentCenter;
|
||||
hStackView.layoutMarginsRelativeArrangement = YES;
|
||||
hStackView.layoutMargins
|
||||
= UIEdgeInsetsMake(OWSContactShareView.vMargin, self.hMargin, OWSContactShareView.vMargin, self.hMargin);
|
||||
[hStackView addArrangedSubview:avatarView];
|
||||
[hStackView addArrangedSubview:labelsView];
|
||||
[hStackView addArrangedSubview:disclosureImageView];
|
||||
[self addSubview:hStackView];
|
||||
[hStackView ows_autoPinToSuperviewEdges];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -7,12 +7,12 @@
|
|||
#import "Session-Swift.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
|
||||
#import <SignalUtilitiesKit/OWSFormat.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/MimeTypeUtil.h>
|
||||
#import <SignalUtilitiesKit/NSString+SSK.h>
|
||||
#import <SignalUtilitiesKit/TSAttachmentStream.h>
|
||||
#import <SessionUtilitiesKit/MIMETypeUtil.h>
|
||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||
#import <SessionMessagingKit/TSAttachmentStream.h>
|
||||
#import <SignalCoreKit/NSString+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
@ -91,7 +91,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (CGFloat)iconHeight
|
||||
{
|
||||
return kStandardAvatarSize;
|
||||
return 48.0f;
|
||||
}
|
||||
|
||||
- (void)createContentsWithConversationStyle:(ConversationStyle *)conversationStyle
|
||||
|
|
|
@ -49,15 +49,6 @@ typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) {
|
|||
|
||||
- (void)didTapConversationItem:(id<ConversationViewItem>)viewItem linkPreview:(OWSLinkPreview *)linkPreview;
|
||||
|
||||
- (void)didTapContactShareViewItem:(id<ConversationViewItem>)viewItem;
|
||||
|
||||
- (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare
|
||||
NS_SWIFT_NAME(didTapSendMessage(toContactShare:));
|
||||
- (void)didTapSendInviteToContactShare:(ContactShareViewModel *)contactShare
|
||||
NS_SWIFT_NAME(didTapSendInvite(toContactShare:));
|
||||
- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare
|
||||
NS_SWIFT_NAME(didTapShowAddToContactUI(forContactShare:));
|
||||
|
||||
@property (nonatomic, readonly, nullable) NSString *lastSearchedText;
|
||||
|
||||
@end
|
||||
|
|
|
@ -7,8 +7,6 @@
|
|||
#import "ConversationViewItem.h"
|
||||
#import "OWSBubbleShapeView.h"
|
||||
#import "OWSBubbleView.h"
|
||||
#import "OWSContactShareButtonsView.h"
|
||||
#import "OWSContactShareView.h"
|
||||
#import "OWSGenericAttachmentView.h"
|
||||
#import "OWSLabel.h"
|
||||
#import "OWSMessageFooterView.h"
|
||||
|
@ -16,11 +14,11 @@
|
|||
#import "OWSQuotedMessageView.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSMessageBubbleView () <OWSQuotedMessageViewDelegate, OWSContactShareButtonsViewDelegate>
|
||||
@interface OWSMessageBubbleView () <OWSQuotedMessageViewDelegate>
|
||||
|
||||
@property (nonatomic) OWSBubbleView *bubbleView;
|
||||
|
||||
|
@ -48,21 +46,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
@property (nonatomic) OWSMessageFooterView *footerView;
|
||||
|
||||
@property (nonatomic, nullable) OWSContactShareButtonsView *contactShareButtonsView;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSMessageBubbleView
|
||||
|
||||
#pragma mark - Dependencies
|
||||
|
||||
- (OWSAttachmentDownloads *)attachmentDownloads
|
||||
{
|
||||
return SSKEnvironment.shared.attachmentDownloads;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
|
@ -267,9 +256,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
case OWSMessageCellType_GenericAttachment:
|
||||
bodyMediaView = [self loadViewForGenericAttachment];
|
||||
break;
|
||||
case OWSMessageCellType_ContactShare:
|
||||
bodyMediaView = [self loadViewForContactShare];
|
||||
break;
|
||||
case OWSMessageCellType_MediaMessage:
|
||||
bodyMediaView = [self loadViewForMediaAlbum];
|
||||
break;
|
||||
|
@ -300,24 +286,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
if (self.hasBodyMediaWithThumbnail) {
|
||||
[self.stackView addArrangedSubview:bodyMediaView];
|
||||
} else {
|
||||
OWSAssertDebug(self.cellType == OWSMessageCellType_ContactShare);
|
||||
|
||||
if (self.contactShareHasSpacerTop) {
|
||||
UIView *spacerView = [UIView containerView];
|
||||
[spacerView autoSetDimension:ALDimensionHeight toSize:self.contactShareVSpacing];
|
||||
[spacerView setCompressionResistanceHigh];
|
||||
[self.stackView addArrangedSubview:spacerView];
|
||||
}
|
||||
|
||||
[self.stackView addArrangedSubview:bodyMediaView];
|
||||
|
||||
if (self.contactShareHasSpacerBottom) {
|
||||
UIView *spacerView = [UIView containerView];
|
||||
[spacerView autoSetDimension:ALDimensionHeight toSize:self.contactShareVSpacing];
|
||||
[spacerView setCompressionResistanceHigh];
|
||||
[self.stackView addArrangedSubview:spacerView];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
[textViews addObject:bodyMediaView];
|
||||
|
@ -414,86 +382,11 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
addObject:[bodyMediaView autoSetDimension:ALDimensionHeight toSize:bodyMediaSize.CGSizeValue.height]];
|
||||
}
|
||||
|
||||
[self insertContactShareButtonsIfNecessary];
|
||||
|
||||
[self updateBubbleColor];
|
||||
|
||||
[self configureBubbleRounding];
|
||||
}
|
||||
|
||||
- (void)insertContactShareButtonsIfNecessary
|
||||
{
|
||||
if (self.cellType != OWSMessageCellType_ContactShare) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (![OWSContactShareButtonsView hasAnyButton:self.viewItem.contactShare]) {
|
||||
return;
|
||||
}
|
||||
|
||||
OWSAssertDebug(self.viewItem.contactShare);
|
||||
|
||||
OWSContactShareButtonsView *buttonsView =
|
||||
[[OWSContactShareButtonsView alloc] initWithContactShare:self.viewItem.contactShare delegate:self];
|
||||
|
||||
NSValue *_Nullable actionButtonsSize = [self actionButtonsSize];
|
||||
OWSAssertDebug(actionButtonsSize);
|
||||
[self.viewConstraints addObjectsFromArray:@[
|
||||
[buttonsView autoSetDimension:ALDimensionHeight toSize:actionButtonsSize.CGSizeValue.height],
|
||||
]];
|
||||
|
||||
// The "contact share" view casts a shadow "downward" onto adjacent views,
|
||||
// so we use a "proxy" view to take its place within the v-stack
|
||||
// view and then insert the "contact share" view above its proxy so that
|
||||
// it floats above the other content of the bubble view.
|
||||
|
||||
UIView *proxyView = [UIView new];
|
||||
[self.stackView addArrangedSubview:proxyView];
|
||||
|
||||
OWSBubbleShapeView *shadowView = [[OWSBubbleShapeView alloc] initShadow];
|
||||
OWSBubbleShapeView *clipView = [[OWSBubbleShapeView alloc] initClip];
|
||||
|
||||
[self addSubview:shadowView];
|
||||
[self addSubview:clipView];
|
||||
|
||||
[self.viewConstraints addObjectsFromArray:[shadowView autoPinToEdgesOfView:proxyView]];
|
||||
[self.viewConstraints addObjectsFromArray:[clipView autoPinToEdgesOfView:proxyView]];
|
||||
|
||||
[clipView addSubview:buttonsView];
|
||||
[self.viewConstraints addObjectsFromArray:[buttonsView ows_autoPinToSuperviewEdges]];
|
||||
|
||||
[self.bubbleView addPartnerView:shadowView];
|
||||
[self.bubbleView addPartnerView:clipView];
|
||||
|
||||
// Prevent the layer from animating changes.
|
||||
[CATransaction begin];
|
||||
[CATransaction setDisableActions:YES];
|
||||
|
||||
OWSAssertDebug(buttonsView.backgroundColor);
|
||||
shadowView.fillColor = buttonsView.backgroundColor;
|
||||
shadowView.layer.shadowColor = Theme.boldColor.CGColor;
|
||||
shadowView.layer.shadowOpacity = 0.12f;
|
||||
shadowView.layer.shadowOffset = CGSizeZero;
|
||||
shadowView.layer.shadowRadius = 1.f;
|
||||
|
||||
[CATransaction commit];
|
||||
}
|
||||
|
||||
- (BOOL)contactShareHasSpacerTop
|
||||
{
|
||||
return (self.cellType == OWSMessageCellType_ContactShare && (self.isQuotedReply || !self.shouldShowSenderName));
|
||||
}
|
||||
|
||||
- (BOOL)contactShareHasSpacerBottom
|
||||
{
|
||||
return (self.cellType == OWSMessageCellType_ContactShare && !self.hasBottomFooter);
|
||||
}
|
||||
|
||||
- (CGFloat)contactShareVSpacing
|
||||
{
|
||||
return 12.f;
|
||||
}
|
||||
|
||||
- (CGFloat)senderNameBottomSpacing
|
||||
{
|
||||
return 0.f;
|
||||
|
@ -557,7 +450,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
case OWSMessageCellType_OversizeTextDownloading:
|
||||
return NO;
|
||||
case OWSMessageCellType_MediaMessage:
|
||||
|
@ -572,7 +464,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return NO;
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
case OWSMessageCellType_MediaMessage:
|
||||
case OWSMessageCellType_OversizeTextDownloading:
|
||||
return YES;
|
||||
|
@ -581,8 +472,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
- (BOOL)hasFullWidthMediaView
|
||||
{
|
||||
return (self.hasBodyMediaWithThumbnail || self.cellType == OWSMessageCellType_ContactShare
|
||||
|| self.cellType == OWSMessageCellType_MediaMessage);
|
||||
return (self.hasBodyMediaWithThumbnail || self.cellType == OWSMessageCellType_MediaMessage);
|
||||
}
|
||||
|
||||
- (BOOL)canFooterOverlayMedia
|
||||
|
@ -876,26 +766,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return attachmentView;
|
||||
}
|
||||
|
||||
- (UIView *)loadViewForContactShare
|
||||
{
|
||||
OWSAssertDebug(self.viewItem.contactShare);
|
||||
|
||||
OWSContactShareView *contactShareView = [[OWSContactShareView alloc] initWithContactShare:self.viewItem.contactShare
|
||||
isIncoming:self.isIncoming
|
||||
conversationStyle:self.conversationStyle];
|
||||
[contactShareView createContents];
|
||||
// TODO: Should we change appearance if contact avatar is uploading?
|
||||
|
||||
self.loadCellContentBlock = ^{
|
||||
// Do nothing.
|
||||
};
|
||||
self.unloadCellContentBlock = ^{
|
||||
// Do nothing.
|
||||
};
|
||||
|
||||
return contactShareView;
|
||||
}
|
||||
|
||||
- (UIView *)loadViewForOversizeTextDownload
|
||||
{
|
||||
// We can use an empty view. The progress views will display download
|
||||
|
@ -969,10 +839,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSFailDebug(@"Missing uniqueId.");
|
||||
return;
|
||||
}
|
||||
if ([self.attachmentDownloads downloadProgressForAttachmentId:uniqueId] == nil) {
|
||||
OWSFailDebug(@"Missing download progress.");
|
||||
return;
|
||||
}
|
||||
|
||||
UIView *overlayView = [UIView new];
|
||||
overlayView.backgroundColor = [self.bubbleColor colorWithAlphaComponent:0.5];
|
||||
|
@ -1079,11 +945,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
result = [attachmentView measureSizeWithMaxMessageWidth:maxMessageWidth];
|
||||
break;
|
||||
}
|
||||
case OWSMessageCellType_ContactShare:
|
||||
OWSAssertDebug(self.viewItem.contactShare);
|
||||
|
||||
result = CGSizeMake(maxMessageWidth, [OWSContactShareView bubbleHeight]);
|
||||
break;
|
||||
case OWSMessageCellType_MediaMessage:
|
||||
result = [OWSMediaAlbumCellView layoutSizeForMaxMessageWidth:maxMessageWidth
|
||||
items:self.viewItem.mediaAlbumItems];
|
||||
|
@ -1186,23 +1047,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return [NSValue valueWithCGSize:result];
|
||||
}
|
||||
|
||||
- (nullable NSValue *)actionButtonsSize
|
||||
{
|
||||
OWSAssertDebug(self.conversationStyle);
|
||||
OWSAssertDebug(self.conversationStyle.maxMessageWidth > 0);
|
||||
|
||||
if (self.cellType == OWSMessageCellType_ContactShare) {
|
||||
OWSAssertDebug(self.viewItem.contactShare);
|
||||
|
||||
if ([OWSContactShareButtonsView hasAnyButton:self.viewItem.contactShare]) {
|
||||
CGSize buttonsSize = CGSizeCeil(
|
||||
CGSizeMake(self.conversationStyle.maxMessageWidth, [OWSContactShareButtonsView bubbleHeight]));
|
||||
return [NSValue valueWithCGSize:buttonsSize];
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (CGSize)measureSize
|
||||
{
|
||||
OWSAssertDebug(self.conversationStyle);
|
||||
|
@ -1239,13 +1083,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[textViewSizes addObject:bodyMediaSize];
|
||||
bodyMediaSize = nil;
|
||||
}
|
||||
|
||||
if (self.contactShareHasSpacerTop) {
|
||||
cellSize.height += self.contactShareVSpacing;
|
||||
}
|
||||
if (self.contactShareHasSpacerBottom) {
|
||||
cellSize.height += self.contactShareVSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
if (bodyMediaSize || quotedMessageSize) {
|
||||
|
@ -1296,12 +1133,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
cellSize.height += self.tapForMoreHeight + self.textViewVSpacing;
|
||||
}
|
||||
|
||||
NSValue *_Nullable actionButtonsSize = [self actionButtonsSize];
|
||||
if (actionButtonsSize) {
|
||||
cellSize.width = MAX(cellSize.width, actionButtonsSize.CGSizeValue.width);
|
||||
cellSize.height += actionButtonsSize.CGSizeValue.height;
|
||||
}
|
||||
|
||||
cellSize = CGSizeCeil(cellSize);
|
||||
|
||||
OWSAssertDebug(cellSize.width <= self.conversationStyle.maxMessageWidth);
|
||||
|
@ -1395,9 +1226,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
}
|
||||
|
||||
[self.contactShareButtonsView removeFromSuperview];
|
||||
self.contactShareButtonsView = nil;
|
||||
|
||||
[self.linkPreviewView removeFromSuperview];
|
||||
self.linkPreviewView.state = nil;
|
||||
}
|
||||
|
@ -1430,12 +1258,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
}
|
||||
|
||||
if (self.contactShareButtonsView) {
|
||||
if ([self.contactShareButtonsView handleTapGesture:sender]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CGPoint locationInMessageBubble = [sender locationInView:self];
|
||||
switch ([self gestureLocationForLocation:locationInMessageBubble]) {
|
||||
case OWSMessageGestureLocation_Default:
|
||||
|
@ -1488,9 +1310,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[AttachmentSharing showShareUIForAttachment:self.viewItem.attachmentStream];
|
||||
}
|
||||
break;
|
||||
case OWSMessageCellType_ContactShare:
|
||||
[self.delegate didTapContactShareViewItem:self.viewItem];
|
||||
break;
|
||||
case OWSMessageCellType_MediaMessage: {
|
||||
OWSAssertDebug(self.bodyMediaView);
|
||||
OWSAssertDebug(self.viewItem.mediaAlbumItems.count > 0);
|
||||
|
@ -1603,32 +1422,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSFailDebug(@"Sent quoted replies should not be cancellable.");
|
||||
}
|
||||
|
||||
#pragma mark - OWSContactShareButtonsViewDelegate
|
||||
|
||||
- (void)didTapSendMessageToContactShare:(ContactShareViewModel *)contactShare
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
[self.delegate didTapSendMessageToContactShare:contactShare];
|
||||
}
|
||||
|
||||
- (void)didTapSendInviteToContactShare:(ContactShareViewModel *)contactShare
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
[self.delegate didTapSendInviteToContactShare:contactShare];
|
||||
}
|
||||
|
||||
- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(contactShare);
|
||||
|
||||
[self.delegate didTapShowAddToContactUIForContactShare:contactShare];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
//
|
||||
|
||||
#import "OWSMessageCell.h"
|
||||
#import "OWSContactAvatarBuilder.h"
|
||||
#import "OWSMessageBubbleView.h"
|
||||
#import "OWSMessageHeaderView.h"
|
||||
#import "Session-Swift.h"
|
||||
|
@ -286,11 +285,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[self.avatarView update];
|
||||
|
||||
// Loki: Show the moderator icon if needed
|
||||
if (self.viewItem.isGroupThread && !self.viewItem.isRSSFeed) { // FIXME: This logic also shouldn't apply to closed groups
|
||||
__block SNOpenGroup *publicChat;
|
||||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:self.viewItem.interaction.uniqueThreadId transaction: transaction];
|
||||
}];
|
||||
if (self.viewItem.isGroupThread) { // FIXME: This logic also shouldn't apply to closed groups
|
||||
SNOpenGroup *publicChat = [LKStorage.shared getOpenGroupForThreadID:self.viewItem.interaction.uniqueThreadId];
|
||||
if (publicChat != nil) {
|
||||
BOOL isModerator = [SNOpenGroupAPI isUserModerator:incomingMessage.authorId forChannel:publicChat.channel onServer:publicChat.server];
|
||||
UIImage *moderatorIcon = [UIImage imageNamed:@"Crown"];
|
||||
|
|
|
@ -122,7 +122,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
TSContactThread *thread = [outgoingMessage.thread as:TSContactThread.class];
|
||||
if (thread != nil) {
|
||||
isNoteToSelf = [LKDatabaseUtilities isUserLinkedDevice:thread.contactIdentifier in:transaction];
|
||||
NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
|
||||
isNoteToSelf = ([thread.contactIdentifier isEqual:userPublicKey]);
|
||||
}
|
||||
}];
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
#import <SignalUtilitiesKit/OWSUnreadIndicator.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIFont+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
//
|
||||
|
||||
#import "OWSMessageTextView.h"
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
#import "UIView+OWS.h"
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalUtilitiesKit/NSTimer+OWS.h>
|
||||
#import <SessionUtilitiesKit/NSTimer+Proxying.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
#import "OWSBubbleView.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalCoreKit/NSString+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SignalUtilitiesKit/TSAttachmentStream.h>
|
||||
#import <SignalUtilitiesKit/TSMessage.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionMessagingKit/TSAttachmentStream.h>
|
||||
#import <SessionMessagingKit/TSMessage.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
@ -543,23 +543,15 @@ const CGFloat kRemotelySourcedContentRowSpacing = 4;
|
|||
NSString *_Nullable localNumber = [TSAccountManager localNumber];
|
||||
NSString *quotedAuthorText;
|
||||
if ([localNumber isEqualToString:self.quotedMessage.authorId]) {
|
||||
|
||||
if (self.isOutgoing) {
|
||||
quotedAuthorText = NSLocalizedString(@"You", @"");
|
||||
} else {
|
||||
quotedAuthorText = NSLocalizedString(@"You", @"");
|
||||
}
|
||||
quotedAuthorText = NSLocalizedString(@"You", @"");
|
||||
} else {
|
||||
OWSContactsManager *contactsManager = Environment.shared.contactsManager;
|
||||
__block NSString *quotedAuthor = [SSKEnvironment.shared.profileManager profileNameForRecipientWithID:self.quotedMessage.authorId] ?: [contactsManager contactOrProfileNameForPhoneIdentifier:self.quotedMessage.authorId];
|
||||
__block NSString *quotedAuthor = [SSKEnvironment.shared.profileManager profileNameForRecipientWithID:self.quotedMessage.authorId] ?: self.quotedMessage.authorId;
|
||||
|
||||
if (quotedAuthor == self.quotedMessage.authorId) {
|
||||
SNOpenGroup *openGroup = [LKStorage.shared getOpenGroupForThreadID:self.quotedMessage.threadId];
|
||||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
SNOpenGroup *publicChat = [LKDatabaseUtilities getPublicChatForThreadID:self.quotedMessage.threadId transaction:transaction];
|
||||
if (publicChat != nil) {
|
||||
quotedAuthor = [LKUserDisplayNameUtilities getPublicChatDisplayNameFor:self.quotedMessage.authorId in:publicChat.channel on:publicChat.server using:transaction];
|
||||
} else {
|
||||
quotedAuthor = [LKUserDisplayNameUtilities getPrivateChatDisplayNameFor:self.quotedMessage.authorId];
|
||||
if (openGroup != nil) {
|
||||
quotedAuthor = [LKUserDisplayNameUtilities getPublicChatDisplayNameFor:self.quotedMessage.authorId in:openGroup.channel on:openGroup.server using:transaction];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -9,12 +9,10 @@
|
|||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/OWSVerificationStateChangeMessage.h>
|
||||
#import <SignalUtilitiesKit/TSCall.h>
|
||||
#import <SignalUtilitiesKit/TSErrorMessage.h>
|
||||
#import <SignalUtilitiesKit/TSInfoMessage.h>
|
||||
#import <SessionMessagingKit/OWSDisappearingConfigurationUpdateInfoMessage.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SessionMessagingKit/TSErrorMessage.h>
|
||||
#import <SessionMessagingKit/TSInfoMessage.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
@ -290,20 +288,7 @@ typedef void (^SystemMessageActionBlock)(void);
|
|||
: [UIImage imageNamed:@"system_message_disappearing_messages_disabled"]);
|
||||
break;
|
||||
}
|
||||
case TSInfoMessageVerificationStateChange:
|
||||
OWSAssertDebug([interaction isKindOfClass:[OWSVerificationStateChangeMessage class]]);
|
||||
if ([interaction isKindOfClass:[OWSVerificationStateChangeMessage class]]) {
|
||||
OWSVerificationStateChangeMessage *message = (OWSVerificationStateChangeMessage *)interaction;
|
||||
BOOL isVerified = message.verificationState == OWSVerificationStateVerified;
|
||||
if (!isVerified) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
result = [UIImage imageNamed:@"system_message_verified"];
|
||||
break;
|
||||
}
|
||||
} else if ([interaction isKindOfClass:[TSCall class]]) {
|
||||
result = [UIImage imageNamed:@"system_message_call"];
|
||||
} else {
|
||||
OWSFailDebug(@"Unknown interaction type: %@", [interaction class]);
|
||||
return nil;
|
||||
|
@ -398,8 +383,6 @@ typedef void (^SystemMessageActionBlock)(void);
|
|||
return [self actionForErrorMessage:(TSErrorMessage *)interaction];
|
||||
} else if ([interaction isKindOfClass:[TSInfoMessage class]]) {
|
||||
return [self actionForInfoMessage:(TSInfoMessage *)interaction];
|
||||
} else if ([interaction isKindOfClass:[TSCall class]]) {
|
||||
return [self actionForCall:(TSCall *)interaction];
|
||||
} else {
|
||||
OWSFailDebug(@"Tap for system messages of unknown type: %@", [interaction class]);
|
||||
return nil;
|
||||
|
@ -414,21 +397,6 @@ typedef void (^SystemMessageActionBlock)(void);
|
|||
switch (message.errorType) {
|
||||
case TSErrorMessageInvalidKeyException:
|
||||
return nil;
|
||||
case TSErrorMessageNonBlockingIdentityChange:
|
||||
return [SystemMessageAction
|
||||
actionWithTitle:NSLocalizedString(@"SYSTEM_MESSAGE_ACTION_VERIFY_SAFETY_NUMBER",
|
||||
@"Label for button to verify a user's safety number.")
|
||||
block:^{
|
||||
[weakSelf.delegate tappedNonBlockingIdentityChangeForRecipientId:message.recipientId];
|
||||
}];
|
||||
case TSErrorMessageWrongTrustedIdentityKey:
|
||||
return [SystemMessageAction
|
||||
actionWithTitle:NSLocalizedString(@"SYSTEM_MESSAGE_ACTION_VERIFY_SAFETY_NUMBER",
|
||||
@"Label for button to verify a user's safety number.")
|
||||
block:^{
|
||||
[weakSelf.delegate
|
||||
tappedInvalidIdentityKeyErrorMessage:(TSInvalidIdentityKeyErrorMessage *)message];
|
||||
}];
|
||||
case TSErrorMessageMissingKeyId:
|
||||
case TSErrorMessageNoSession:
|
||||
case TSErrorMessageInvalidMessage:
|
||||
|
@ -486,48 +454,12 @@ typedef void (^SystemMessageActionBlock)(void);
|
|||
block:^{
|
||||
[weakSelf.delegate showConversationSettings];
|
||||
}];
|
||||
case TSInfoMessageVerificationStateChange:
|
||||
return [SystemMessageAction
|
||||
actionWithTitle:NSLocalizedString(@"SHOW_SAFETY_NUMBER_ACTION", @"Action sheet item")
|
||||
block:^{
|
||||
[weakSelf.delegate
|
||||
showFingerprintWithRecipientId:((OWSVerificationStateChangeMessage *)message)
|
||||
.recipientId];
|
||||
}];
|
||||
}
|
||||
|
||||
OWSLogInfo(@"Unhandled tap for info message: %@", message);
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (nullable SystemMessageAction *)actionForCall:(TSCall *)call
|
||||
{
|
||||
OWSAssertDebug(call);
|
||||
|
||||
__weak OWSSystemMessageCell *weakSelf = self;
|
||||
switch (call.callType) {
|
||||
case RPRecentCallTypeIncoming:
|
||||
case RPRecentCallTypeIncomingMissed:
|
||||
case RPRecentCallTypeIncomingMissedBecauseOfChangedIdentity:
|
||||
case RPRecentCallTypeIncomingDeclined:
|
||||
return
|
||||
[SystemMessageAction actionWithTitle:NSLocalizedString(@"CALLBACK_BUTTON_TITLE", @"notification action")
|
||||
block:^{
|
||||
[weakSelf.delegate handleCallTap:call];
|
||||
}];
|
||||
case RPRecentCallTypeOutgoing:
|
||||
case RPRecentCallTypeOutgoingMissed:
|
||||
return [SystemMessageAction actionWithTitle:NSLocalizedString(@"CALL_AGAIN_BUTTON_TITLE",
|
||||
@"Label for button that lets users call a contact again.")
|
||||
block:^{
|
||||
[weakSelf.delegate handleCallTap:call];
|
||||
}];
|
||||
case RPRecentCallTypeOutgoingIncomplete:
|
||||
case RPRecentCallTypeIncomingIncomplete:
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Events
|
||||
|
||||
- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)longPress
|
||||
|
|
|
@ -19,7 +19,7 @@ public class TypingIndicatorCell: ConversationViewCell {
|
|||
private let kAvatarSize: CGFloat = 36
|
||||
private let kAvatarHSpacing: CGFloat = 8
|
||||
|
||||
private let avatarView = AvatarImageView()
|
||||
// private let avatarView = AvatarImageView()
|
||||
private let bubbleView = OWSBubbleView()
|
||||
private let typingIndicatorView = TypingIndicatorView()
|
||||
private var viewConstraints = [NSLayoutConstraint]()
|
||||
|
@ -39,8 +39,8 @@ public class TypingIndicatorCell: ConversationViewCell {
|
|||
bubbleView.addSubview(typingIndicatorView)
|
||||
contentView.addSubview(bubbleView)
|
||||
|
||||
avatarView.autoSetDimension(.width, toSize: kAvatarSize)
|
||||
avatarView.autoSetDimension(.height, toSize: kAvatarSize)
|
||||
// avatarView.autoSetDimension(.width, toSize: kAvatarSize)
|
||||
// avatarView.autoSetDimension(.height, toSize: kAvatarSize)
|
||||
}
|
||||
|
||||
@objc
|
||||
|
@ -65,16 +65,16 @@ public class TypingIndicatorCell: ConversationViewCell {
|
|||
typingIndicatorView.autoPinBottomToSuperviewMargin(withInset: conversationStyle.textInsetBottom)
|
||||
])
|
||||
|
||||
if let avatarView = configureAvatarView() {
|
||||
contentView.addSubview(avatarView)
|
||||
viewConstraints.append(contentsOf: [
|
||||
bubbleView.autoPinLeading(toTrailingEdgeOf: avatarView, offset: kAvatarHSpacing),
|
||||
bubbleView.autoAlignAxis(.horizontal, toSameAxisOf: avatarView)
|
||||
])
|
||||
|
||||
} else {
|
||||
avatarView.removeFromSuperview()
|
||||
}
|
||||
// if let avatarView = configureAvatarView() {
|
||||
// contentView.addSubview(avatarView)
|
||||
// viewConstraints.append(contentsOf: [
|
||||
// bubbleView.autoPinLeading(toTrailingEdgeOf: avatarView, offset: kAvatarHSpacing),
|
||||
// bubbleView.autoAlignAxis(.horizontal, toSameAxisOf: avatarView)
|
||||
// ])
|
||||
//
|
||||
// } else {
|
||||
// avatarView.removeFromSuperview()
|
||||
// }
|
||||
}
|
||||
|
||||
private func configureAvatarView() -> UIView? {
|
||||
|
@ -93,15 +93,16 @@ public class TypingIndicatorCell: ConversationViewCell {
|
|||
owsFailDebug("Missing authorConversationColorName")
|
||||
return nil
|
||||
}
|
||||
guard let authorAvatarImage =
|
||||
OWSContactAvatarBuilder(signalId: typingIndicators.recipientId,
|
||||
colorName: ConversationColorName(rawValue: colorName),
|
||||
diameter: UInt(kAvatarSize)).build() else {
|
||||
owsFailDebug("Could build avatar image")
|
||||
return nil
|
||||
}
|
||||
avatarView.image = authorAvatarImage
|
||||
return avatarView
|
||||
// guard let authorAvatarImage =
|
||||
// OWSContactAvatarBuilder(signalId: typingIndicators.recipientId,
|
||||
// colorName: ConversationColorName(rawValue: colorName),
|
||||
// diameter: UInt(kAvatarSize)).build() else {
|
||||
// owsFailDebug("Could build avatar image")
|
||||
// return nil
|
||||
// }
|
||||
// avatarView.image = authorAvatarImage
|
||||
// return avatarView
|
||||
return UIView()
|
||||
}
|
||||
|
||||
private func shouldShowAvatar() -> Bool {
|
||||
|
@ -140,8 +141,8 @@ public class TypingIndicatorCell: ConversationViewCell {
|
|||
NSLayoutConstraint.deactivate(viewConstraints)
|
||||
viewConstraints = [NSLayoutConstraint]()
|
||||
|
||||
avatarView.image = nil
|
||||
avatarView.removeFromSuperview()
|
||||
// avatarView.image = nil
|
||||
// avatarView.removeFromSuperview()
|
||||
|
||||
typingIndicatorView.stopAnimation()
|
||||
}
|
||||
|
|
|
@ -1,131 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc
|
||||
public protocol ConversationHeaderViewDelegate {
|
||||
func didTapConversationHeaderView(_ conversationHeaderView: ConversationHeaderView)
|
||||
}
|
||||
|
||||
@objc
|
||||
public class ConversationHeaderView: UIStackView {
|
||||
|
||||
@objc
|
||||
public weak var delegate: ConversationHeaderViewDelegate?
|
||||
|
||||
@objc
|
||||
public var attributedTitle: NSAttributedString? {
|
||||
get {
|
||||
return self.titleLabel.attributedText
|
||||
}
|
||||
set {
|
||||
self.titleLabel.attributedText = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public var attributedSubtitle: NSAttributedString? {
|
||||
get {
|
||||
return self.subtitleLabel.attributedText
|
||||
}
|
||||
set {
|
||||
self.subtitleLabel.attributedText = newValue
|
||||
self.subtitleLabel.isHidden = newValue == nil
|
||||
}
|
||||
}
|
||||
|
||||
public var avatarImage: UIImage? {
|
||||
get {
|
||||
return self.avatarView.image
|
||||
}
|
||||
set {
|
||||
self.avatarView.image = newValue
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public let titlePrimaryFont: UIFont = UIFont.ows_boldFont(withSize: 17)
|
||||
@objc
|
||||
public let titleSecondaryFont: UIFont = UIFont.ows_regularFont(withSize: 9)
|
||||
@objc
|
||||
public let subtitleFont: UIFont = UIFont.ows_regularFont(withSize: 12)
|
||||
|
||||
private let titleLabel: UILabel
|
||||
private let subtitleLabel: UILabel
|
||||
private let avatarView: ConversationAvatarImageView
|
||||
|
||||
@objc
|
||||
public required init(thread: TSThread, contactsManager: OWSContactsManager) {
|
||||
|
||||
let avatarView = ConversationAvatarImageView(thread: thread, diameter: 36, contactsManager: contactsManager)
|
||||
self.avatarView = avatarView
|
||||
|
||||
titleLabel = UILabel()
|
||||
titleLabel.textColor = Theme.navbarTitleColor
|
||||
titleLabel.lineBreakMode = .byTruncatingTail
|
||||
titleLabel.font = titlePrimaryFont
|
||||
titleLabel.setContentHuggingHigh()
|
||||
|
||||
subtitleLabel = UILabel()
|
||||
subtitleLabel.textColor = Theme.navbarTitleColor
|
||||
subtitleLabel.lineBreakMode = .byTruncatingTail
|
||||
subtitleLabel.font = subtitleFont
|
||||
subtitleLabel.setContentHuggingHigh()
|
||||
|
||||
let textRows = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
|
||||
textRows.axis = .vertical
|
||||
textRows.alignment = .leading
|
||||
textRows.distribution = .fillProportionally
|
||||
textRows.spacing = 0
|
||||
|
||||
textRows.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
|
||||
textRows.isLayoutMarginsRelativeArrangement = true
|
||||
|
||||
// low content hugging so that the text rows push container to the right bar button item(s)
|
||||
textRows.setContentHuggingLow()
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.layoutMargins = UIEdgeInsets(top: 4, left: 2, bottom: 4, right: 2)
|
||||
self.isLayoutMarginsRelativeArrangement = true
|
||||
|
||||
self.axis = .horizontal
|
||||
self.alignment = .center
|
||||
self.spacing = 0
|
||||
self.addArrangedSubview(avatarView)
|
||||
self.addArrangedSubview(textRows)
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapView))
|
||||
self.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
required public init(coder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
required public override init(frame: CGRect) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
public override var intrinsicContentSize: CGSize {
|
||||
// Grow to fill as much of the navbar as possible.
|
||||
return UIView.layoutFittingExpandedSize
|
||||
}
|
||||
|
||||
@objc
|
||||
public func updateAvatar() {
|
||||
self.avatarView.updateImage()
|
||||
}
|
||||
|
||||
// MARK: Delegate Methods
|
||||
|
||||
@objc func didTapView(tapGesture: UITapGestureRecognizer) {
|
||||
guard tapGesture.state == .recognized else {
|
||||
return
|
||||
}
|
||||
|
||||
self.delegate?.didTapConversationHeaderView(self)
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
#import "ConversationInputTextView.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalUtilitiesKit/NSString+SSK.h>
|
||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||
#import <SignalCoreKit/NSString+OWS.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
|
|
@ -5,18 +5,15 @@
|
|||
#import "ConversationInputToolbar.h"
|
||||
#import "ConversationInputTextView.h"
|
||||
#import "Environment.h"
|
||||
#import "OWSContactsManager.h"
|
||||
#import "OWSMath.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "ViewControllerUtils.h"
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalUtilitiesKit/OWSFormat.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/UIView+OWS.h>
|
||||
#import <SignalUtilitiesKit/NSTimer+OWS.h>
|
||||
#import <SignalUtilitiesKit/TSQuotedMessage.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionMessagingKit/TSQuotedMessage.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
@ -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
|
@ -3,7 +3,7 @@
|
|||
//
|
||||
|
||||
#import "ConversationViewLayout.h"
|
||||
#import "OWSAudioPlayer.h"
|
||||
#import <SessionMessagingKit/OWSAudioPlayer.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
@ -12,7 +12,6 @@ typedef NS_ENUM(NSInteger, OWSMessageCellType) {
|
|||
OWSMessageCellType_TextOnlyMessage,
|
||||
OWSMessageCellType_Audio,
|
||||
OWSMessageCellType_GenericAttachment,
|
||||
OWSMessageCellType_ContactShare,
|
||||
OWSMessageCellType_MediaMessage,
|
||||
OWSMessageCellType_OversizeTextDownloading,
|
||||
};
|
||||
|
@ -67,7 +66,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
|
|||
@property (nonatomic, readonly, nullable) OWSQuotedReplyModel *quotedReply;
|
||||
|
||||
@property (nonatomic, readonly) BOOL isGroupThread;
|
||||
@property (nonatomic, readonly) BOOL isRSSFeed;
|
||||
@property (nonatomic, readonly) BOOL userCanDeleteGroupMessage;
|
||||
|
||||
@property (nonatomic, readonly) BOOL hasBodyText;
|
||||
|
@ -163,7 +161,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType);
|
|||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithInteraction:(TSInteraction *)interaction
|
||||
isGroupThread:(BOOL)isGroupThread
|
||||
isRSSFeed:(BOOL)isRSSFeed
|
||||
transaction:(YapDatabaseReadTransaction *)transaction
|
||||
conversationStyle:(ConversationStyle *)conversationStyle;
|
||||
|
||||
|
|
|
@ -4,18 +4,18 @@
|
|||
|
||||
#import <CoreServices/CoreServices.h>
|
||||
#import "ConversationViewItem.h"
|
||||
#import "OWSContactOffersCell.h"
|
||||
|
||||
#import "OWSMessageCell.h"
|
||||
#import "OWSMessageHeaderView.h"
|
||||
#import "OWSSystemMessageCell.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "AnyPromise.h"
|
||||
#import <SignalUtilitiesKit/OWSUnreadIndicator.h>
|
||||
#import <SignalUtilitiesKit/NSData+Image.h>
|
||||
#import <SignalUtilitiesKit/NSString+SSK.h>
|
||||
#import <SignalUtilitiesKit/OWSContact.h>
|
||||
#import <SignalUtilitiesKit/TSInteraction.h>
|
||||
#import <SignalUtilitiesKit/SSKEnvironment.h>
|
||||
#import <SessionUtilitiesKit/NSData+Image.h>
|
||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||
|
||||
#import <SessionMessagingKit/TSInteraction.h>
|
||||
#import <SessionMessagingKit/SSKEnvironment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
@ -31,8 +31,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
return @"OWSMessageCellType_GenericAttachment";
|
||||
case OWSMessageCellType_Unknown:
|
||||
return @"OWSMessageCellType_Unknown";
|
||||
case OWSMessageCellType_ContactShare:
|
||||
return @"OWSMessageCellType_ContactShare";
|
||||
case OWSMessageCellType_MediaMessage:
|
||||
return @"OWSMessageCellType_MediaMessage";
|
||||
case OWSMessageCellType_OversizeTextDownloading:
|
||||
|
@ -119,7 +117,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
@synthesize interaction = _interaction;
|
||||
@synthesize isFirstInCluster = _isFirstInCluster;
|
||||
@synthesize isGroupThread = _isGroupThread;
|
||||
@synthesize isRSSFeed = _isRSSFeed;
|
||||
@synthesize isLastInCluster = _isLastInCluster;
|
||||
@synthesize lastAudioMessageView = _lastAudioMessageView;
|
||||
@synthesize senderName = _senderName;
|
||||
|
@ -127,7 +124,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
|
||||
- (instancetype)initWithInteraction:(TSInteraction *)interaction
|
||||
isGroupThread:(BOOL)isGroupThread
|
||||
isRSSFeed:(BOOL)isRSSFeed
|
||||
transaction:(YapDatabaseReadTransaction *)transaction
|
||||
conversationStyle:(ConversationStyle *)conversationStyle
|
||||
{
|
||||
|
@ -143,11 +139,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
|
||||
_interaction = interaction;
|
||||
_isGroupThread = isGroupThread;
|
||||
_isRSSFeed = isRSSFeed;
|
||||
_conversationStyle = conversationStyle;
|
||||
|
||||
[self updateAuthorConversationColorNameWithTransaction:transaction];
|
||||
|
||||
[self ensureViewState:transaction];
|
||||
|
||||
return self;
|
||||
|
@ -173,37 +166,11 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
self.linkPreview = nil;
|
||||
self.linkPreviewAttachment = nil;
|
||||
|
||||
[self updateAuthorConversationColorNameWithTransaction:transaction];
|
||||
|
||||
[self clearCachedLayoutState];
|
||||
|
||||
[self ensureViewState:transaction];
|
||||
}
|
||||
|
||||
- (void)updateAuthorConversationColorNameWithTransaction:(YapDatabaseReadTransaction *)transaction
|
||||
{
|
||||
OWSAssertDebug(transaction);
|
||||
|
||||
switch (self.interaction.interactionType) {
|
||||
case OWSInteractionType_TypingIndicator: {
|
||||
OWSTypingIndicatorInteraction *typingIndicator = (OWSTypingIndicatorInteraction *)self.interaction;
|
||||
_authorConversationColorName =
|
||||
[TSContactThread conversationColorNameForRecipientId:typingIndicator.recipientId
|
||||
transaction:transaction];
|
||||
break;
|
||||
}
|
||||
case OWSInteractionType_IncomingMessage: {
|
||||
TSIncomingMessage *incomingMessage = (TSIncomingMessage *)self.interaction;
|
||||
_authorConversationColorName =
|
||||
[TSContactThread conversationColorNameForRecipientId:incomingMessage.authorId transaction:transaction];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
_authorConversationColorName = nil;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (OWSPrimaryStorage *)primaryStorage
|
||||
{
|
||||
return SSKEnvironment.shared.primaryStorage;
|
||||
|
@ -385,9 +352,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
case OWSInteractionType_Call:
|
||||
measurementCell = [OWSSystemMessageCell new];
|
||||
break;
|
||||
case OWSInteractionType_Offer:
|
||||
measurementCell = [OWSContactOffersCell new];
|
||||
break;
|
||||
case OWSInteractionType_TypingIndicator:
|
||||
measurementCell = [OWSTypingIndicatorCell new];
|
||||
break;
|
||||
|
@ -445,10 +409,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
case OWSInteractionType_Call:
|
||||
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSSystemMessageCell cellReuseIdentifier]
|
||||
forIndexPath:indexPath];
|
||||
case OWSInteractionType_Offer:
|
||||
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSContactOffersCell cellReuseIdentifier]
|
||||
forIndexPath:indexPath];
|
||||
|
||||
case OWSInteractionType_TypingIndicator:
|
||||
return [collectionView dequeueReusableCellWithReuseIdentifier:[OWSTypingIndicatorCell cellReuseIdentifier]
|
||||
forIndexPath:indexPath];
|
||||
|
@ -489,6 +449,15 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
[self.lastAudioMessageView setProgress:progress / duration];
|
||||
}
|
||||
|
||||
- (void)showInvalidAudioFileAlert
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[OWSAlerts
|
||||
showErrorAlertWithMessage:NSLocalizedString(@"INVALID_AUDIO_FILE_ALERT_ERROR_MESSAGE",
|
||||
@"Message for the alert indicating that an audio file is invalid.")];
|
||||
}
|
||||
|
||||
#pragma mark - Displayable Text
|
||||
|
||||
// TODO: Now that we're caching the displayable text on the view items,
|
||||
|
@ -609,12 +578,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
self.hasViewState = YES;
|
||||
|
||||
TSMessage *message = (TSMessage *)self.interaction;
|
||||
if (message.contactShare) {
|
||||
self.contactShare =
|
||||
[[ContactShareViewModel alloc] initWithContactShareRecord:message.contactShare transaction:transaction];
|
||||
self.messageCellType = OWSMessageCellType_ContactShare;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for quoted replies _before_ media album handling,
|
||||
// since that logic may exit early.
|
||||
|
@ -804,39 +767,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
}
|
||||
case OWSInteractionType_Info: {
|
||||
TSInfoMessage *infoMessage = (TSInfoMessage *)self.interaction;
|
||||
if ([infoMessage isKindOfClass:[OWSVerificationStateChangeMessage class]]) {
|
||||
OWSVerificationStateChangeMessage *verificationMessage
|
||||
= (OWSVerificationStateChangeMessage *)infoMessage;
|
||||
BOOL isVerified = verificationMessage.verificationState == OWSVerificationStateVerified;
|
||||
NSString *displayName =
|
||||
[Environment.shared.contactsManager displayNameForPhoneIdentifier:verificationMessage.recipientId];
|
||||
NSString *titleFormat = (isVerified
|
||||
? (verificationMessage.isLocalChange
|
||||
? NSLocalizedString(@"VERIFICATION_STATE_CHANGE_FORMAT_VERIFIED_LOCAL",
|
||||
@"Format for info message indicating that the verification state was verified "
|
||||
@"on "
|
||||
@"this device. Embeds {{user's name or phone number}}.")
|
||||
: NSLocalizedString(@"VERIFICATION_STATE_CHANGE_FORMAT_VERIFIED_OTHER_DEVICE",
|
||||
@"Format for info message indicating that the verification state was verified "
|
||||
@"on "
|
||||
@"another device. Embeds {{user's name or phone number}}."))
|
||||
: (verificationMessage.isLocalChange
|
||||
? NSLocalizedString(@"VERIFICATION_STATE_CHANGE_FORMAT_NOT_VERIFIED_LOCAL",
|
||||
@"Format for info message indicating that the verification state was "
|
||||
@"unverified on "
|
||||
@"this device. Embeds {{user's name or phone number}}.")
|
||||
: NSLocalizedString(@"VERIFICATION_STATE_CHANGE_FORMAT_NOT_VERIFIED_OTHER_DEVICE",
|
||||
@"Format for info message indicating that the verification state was "
|
||||
@"unverified on "
|
||||
@"another device. Embeds {{user's name or phone number}}.")));
|
||||
return [NSString stringWithFormat:titleFormat, displayName];
|
||||
} else {
|
||||
return [infoMessage previewTextWithTransaction:transaction];
|
||||
}
|
||||
}
|
||||
case OWSInteractionType_Call: {
|
||||
TSCall *call = (TSCall *)self.interaction;
|
||||
return [call previewTextWithTransaction:transaction];
|
||||
return [infoMessage previewTextWithTransaction:transaction];
|
||||
}
|
||||
default:
|
||||
OWSFailDebug(@"not a system message.");
|
||||
|
@ -921,11 +852,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
OWSFailDebug(@"No text to copy");
|
||||
break;
|
||||
}
|
||||
case OWSMessageCellType_ContactShare: {
|
||||
// TODO: Implement copy contact.
|
||||
OWSFailDebug(@"Not implemented yet");
|
||||
break;
|
||||
}
|
||||
case OWSMessageCellType_OversizeTextDownloading:
|
||||
OWSFailDebug(@"Can't copy not-yet-downloaded attachment");
|
||||
return;
|
||||
|
@ -942,10 +868,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare: {
|
||||
OWSFailDebug(@"No media to copy");
|
||||
break;
|
||||
}
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_GenericAttachment: {
|
||||
[self copyAttachmentToPasteboard:self.attachmentStream];
|
||||
|
@ -996,9 +918,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
OWSFailDebug(@"No media to share.");
|
||||
break;
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
[AttachmentSharing showShareUIForAttachment:self.attachmentStream];
|
||||
|
@ -1035,8 +954,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
return NO;
|
||||
case OWSMessageCellType_Audio:
|
||||
return NO;
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
|
@ -1064,8 +981,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
return NO;
|
||||
case OWSMessageCellType_Audio:
|
||||
return NO;
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
|
@ -1104,9 +1019,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
OWSFailDebug(@"Cannot save text data.");
|
||||
break;
|
||||
case OWSMessageCellType_Audio:
|
||||
OWSFailDebug(@"Cannot save media data.");
|
||||
break;
|
||||
|
@ -1172,30 +1084,37 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
|
||||
- (void)deleteAction
|
||||
{
|
||||
[self.interaction remove];
|
||||
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[self.interaction removeWithTransaction:transaction];
|
||||
if (self.interaction.interactionType == OWSInteractionType_OutgoingMessage) {
|
||||
[LKStorage.shared cancelPendingMessageSendJobIfNeededForMessage:self.interaction.timestamp using:transaction];
|
||||
}
|
||||
}];
|
||||
|
||||
if (self.isGroupThread) {
|
||||
// Skip if the thread is an RSS feed
|
||||
TSGroupThread *groupThread = (TSGroupThread *)self.interaction.thread;
|
||||
if (groupThread.isRSSFeed) return;
|
||||
|
||||
// Only allow deletion on incoming and outgoing messages
|
||||
OWSInteractionType interationType = self.interaction.interactionType;
|
||||
if (interationType != OWSInteractionType_IncomingMessage && interationType != OWSInteractionType_OutgoingMessage) return;
|
||||
|
||||
// Make sure it's a public chat message
|
||||
// Make sure it's an open group message
|
||||
TSMessage *message = (TSMessage *)self.interaction;
|
||||
if (!message.isOpenGroupMessage) return;
|
||||
|
||||
__block SNOpenGroup *publicChat;
|
||||
[self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:groupThread.uniqueId transaction: transaction];
|
||||
}];
|
||||
if (publicChat == nil) return;
|
||||
|
||||
// Get the open group
|
||||
SNOpenGroup *openGroup = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId];
|
||||
if (openGroup == nil) return;
|
||||
|
||||
// If it's an incoming message the user must have moderator status
|
||||
if (self.interaction.interactionType == OWSInteractionType_IncomingMessage) {
|
||||
NSString *userPublicKey = [LKStorage.shared getUserPublicKey];
|
||||
if (![SNOpenGroupAPI isUserModerator:userPublicKey forChannel:openGroup.channel onServer:openGroup.server]) { return; }
|
||||
}
|
||||
|
||||
// Delete the message
|
||||
BOOL isSentByUser = (interationType == OWSInteractionType_OutgoingMessage);
|
||||
[[SNOpenGroupAPI deleteMessageWithID:message.openGroupServerMessageID forGroup:publicChat.channel onServer:publicChat.server isSentByUser:isSentByUser].catch(^(NSError *error) {
|
||||
BOOL wasSentByUser = (interationType == OWSInteractionType_OutgoingMessage);
|
||||
[[SNOpenGroupAPI deleteMessageWithID:message.openGroupServerMessageID forGroup:openGroup.channel onServer:openGroup.server isSentByUser:wasSentByUser].catch(^(NSError *error) {
|
||||
// Roll back
|
||||
[self.interaction save];
|
||||
}) retainUntilComplete];
|
||||
|
@ -1217,8 +1136,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
switch (self.messageCellType) {
|
||||
case OWSMessageCellType_Unknown:
|
||||
case OWSMessageCellType_TextOnlyMessage:
|
||||
case OWSMessageCellType_ContactShare:
|
||||
return NO;
|
||||
case OWSMessageCellType_Audio:
|
||||
case OWSMessageCellType_GenericAttachment:
|
||||
return self.attachmentStream != nil;
|
||||
|
@ -1252,7 +1169,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
|
||||
// Ensure the thread is a public chat and not an RSS feed
|
||||
TSGroupThread *groupThread = (TSGroupThread *)self.interaction.thread;
|
||||
if (groupThread.isRSSFeed) return false;
|
||||
|
||||
// Only allow deletion on incoming and outgoing messages
|
||||
OWSInteractionType interationType = self.interaction.interactionType;
|
||||
|
@ -1263,18 +1179,14 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
if (!message.isOpenGroupMessage) return true;
|
||||
|
||||
// Ensure we have the details needed to contact the server
|
||||
__block SNOpenGroup *publicChat;
|
||||
[self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
publicChat = [LKDatabaseUtilities getPublicChatForThreadID:groupThread.uniqueId transaction: transaction];
|
||||
}];
|
||||
SNOpenGroup *publicChat = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId];
|
||||
if (publicChat == nil) return true;
|
||||
|
||||
if (interationType == OWSInteractionType_IncomingMessage) {
|
||||
// Only allow deletion on incoming messages if the user has moderation permission
|
||||
return [SNOpenGroupAPI isUserModerator:self.userHexEncodedPublicKey forChannel:publicChat.channel onServer:publicChat.server];
|
||||
} else {
|
||||
// Only allow deletion on outgoing messages if the user was the sender (i.e. it was not sent from another linked device)
|
||||
return [self.interaction.actualSenderHexEncodedPublicKey isEqual:self.userHexEncodedPublicKey];
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -98,12 +98,10 @@ typedef NS_ENUM(NSUInteger, ConversationUpdateItemType) {
|
|||
@property (nonatomic, readonly) ConversationViewState *viewState;
|
||||
@property (nonatomic, nullable) NSString *focusMessageIdOnOpen;
|
||||
@property (nonatomic, readonly, nullable) ThreadDynamicInteractions *dynamicInteractions;
|
||||
@property (nonatomic, readonly) BOOL isRSSFeed;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithThread:(TSThread *)thread
|
||||
focusMessageIdOnOpen:(nullable NSString *)focusMessageIdOnOpen
|
||||
isRSSFeed:(BOOL)isRSSFeed
|
||||
delegate:(id<ConversationViewModelDelegate>)delegate NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
- (void)ensureDynamicInteractionsAndUpdateIfNecessary:(BOOL)updateIfNecessary;
|
||||
|
|
|
@ -9,20 +9,17 @@
|
|||
#import "OWSQuotedReplyModel.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSContactOffersInteraction.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/OWSUnreadIndicator.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/ThreadUtil.h>
|
||||
#import <SignalUtilitiesKit/OWSBlockingManager.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage.h>
|
||||
#import <SignalUtilitiesKit/SSKEnvironment.h>
|
||||
#import <SignalUtilitiesKit/TSDatabaseView.h>
|
||||
#import <SignalUtilitiesKit/TSIncomingMessage.h>
|
||||
#import <SignalUtilitiesKit/TSOutgoingMessage.h>
|
||||
#import <SignalUtilitiesKit/TSThread.h>
|
||||
#import <SignalUtilitiesKit/TSGroupThread.h>
|
||||
#import <SignalUtilitiesKit/TSGroupModel.h>
|
||||
#import <SessionMessagingKit/OWSBlockingManager.h>
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
#import <SessionMessagingKit/SSKEnvironment.h>
|
||||
#import <SessionMessagingKit/TSDatabaseView.h>
|
||||
#import <SessionMessagingKit/TSIncomingMessage.h>
|
||||
#import <SessionMessagingKit/TSOutgoingMessage.h>
|
||||
#import <SessionMessagingKit/TSThread.h>
|
||||
#import <SessionMessagingKit/TSGroupThread.h>
|
||||
#import <SessionMessagingKit/TSGroupModel.h>
|
||||
#import <YapDatabase/YapDatabase.h>
|
||||
#import <YapDatabase/YapDatabaseAutoView.h>
|
||||
#import <YapDatabase/YapDatabaseViewChange.h>
|
||||
|
@ -226,7 +223,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
|
||||
- (instancetype)initWithThread:(TSThread *)thread
|
||||
focusMessageIdOnOpen:(nullable NSString *)focusMessageIdOnOpen
|
||||
isRSSFeed:(BOOL)isRSSFeed
|
||||
delegate:(id<ConversationViewModelDelegate>)delegate
|
||||
{
|
||||
self = [super init];
|
||||
|
@ -242,7 +238,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
_persistedViewItems = @[];
|
||||
_unsavedOutgoingMessages = @[];
|
||||
self.focusMessageIdOnOpen = focusMessageIdOnOpen;
|
||||
_isRSSFeed = isRSSFeed;
|
||||
_viewState = [[ConversationViewState alloc] initWithViewItems:@[]];
|
||||
|
||||
[self configure];
|
||||
|
@ -269,11 +264,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
return self.primaryStorage.dbReadWriteConnection;
|
||||
}
|
||||
|
||||
- (OWSContactsManager *)contactsManager
|
||||
{
|
||||
return (OWSContactsManager *)SSKEnvironment.shared.contactsManager;
|
||||
}
|
||||
|
||||
- (OWSBlockingManager *)blockingManager
|
||||
{
|
||||
return OWSBlockingManager.sharedManager;
|
||||
|
@ -304,10 +294,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
selector:@selector(applicationDidEnterBackground:)
|
||||
name:OWSApplicationDidEnterBackgroundNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(signalAccountsDidChange:)
|
||||
name:OWSContactsManagerSignalAccountsDidChangeNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(typingIndicatorStateDidChange:)
|
||||
name:[OWSTypingIndicatorsImpl typingIndicatorStateDidChange]
|
||||
|
@ -530,7 +516,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
|
||||
ThreadDynamicInteractions *dynamicInteractions =
|
||||
[ThreadUtil ensureDynamicInteractionsForThread:self.thread
|
||||
contactsManager:self.contactsManager
|
||||
blockingManager:self.blockingManager
|
||||
dbConnection:self.editingDatabaseConnection
|
||||
hideUnreadMessagesIndicator:self.hasClearedUnreadMessagesIndicator
|
||||
|
@ -1023,24 +1008,10 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
return;
|
||||
}
|
||||
|
||||
// Many OWSProfileManager methods aren't safe to call from inside a database
|
||||
// transaction, so do this work now.
|
||||
//
|
||||
// TODO: It'd be nice if these methods took a transaction.
|
||||
BOOL hasLocalProfile = [self.profileManager hasLocalProfile];
|
||||
BOOL isThreadInProfileWhitelist = [self.profileManager isThreadInProfileWhitelist:self.thread];
|
||||
BOOL hasUnwhitelistedMember = NO;
|
||||
for (NSString *recipientId in self.thread.recipientIdentifiers) {
|
||||
if (![self.profileManager isUserInProfileWhitelist:recipientId]) {
|
||||
hasUnwhitelistedMember = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ConversationProfileState *conversationProfileState = [ConversationProfileState new];
|
||||
conversationProfileState.hasLocalProfile = hasLocalProfile;
|
||||
conversationProfileState.isThreadInProfileWhitelist = isThreadInProfileWhitelist;
|
||||
conversationProfileState.hasUnwhitelistedMember = hasUnwhitelistedMember;
|
||||
conversationProfileState.hasLocalProfile = YES;
|
||||
conversationProfileState.isThreadInProfileWhitelist = YES;
|
||||
conversationProfileState.hasUnwhitelistedMember = NO;
|
||||
self.conversationProfileState = conversationProfileState;
|
||||
}
|
||||
|
||||
|
@ -1067,142 +1038,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
return nil;
|
||||
}
|
||||
|
||||
- (nullable OWSContactOffersInteraction *)
|
||||
tryToBuildContactOffersInteractionWithTransaction:(YapDatabaseReadTransaction *)transaction
|
||||
loadedInteractions:(NSArray<TSInteraction *> *)loadedInteractions
|
||||
canLoadMoreItems:(BOOL)canLoadMoreItems
|
||||
{
|
||||
OWSAssertDebug(transaction);
|
||||
OWSAssertDebug(self.conversationProfileState);
|
||||
|
||||
if (canLoadMoreItems) {
|
||||
// Only show contact offers at the start of the conversation.
|
||||
return nil;
|
||||
}
|
||||
|
||||
BOOL hasLocalProfile = self.conversationProfileState.hasLocalProfile;
|
||||
BOOL isThreadInProfileWhitelist = self.conversationProfileState.isThreadInProfileWhitelist;
|
||||
BOOL hasUnwhitelistedMember = self.conversationProfileState.hasUnwhitelistedMember;
|
||||
|
||||
TSThread *thread = self.thread;
|
||||
BOOL isContactThread = [thread isKindOfClass:[TSContactThread class]];
|
||||
if (!isContactThread) {
|
||||
return nil;
|
||||
}
|
||||
TSContactThread *contactThread = (TSContactThread *)thread;
|
||||
if (contactThread.hasDismissedOffers) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *localNumber = [self.tsAccountManager localNumber];
|
||||
OWSAssertDebug(localNumber.length > 0);
|
||||
|
||||
TSInteraction *firstCallOrMessage = [self firstCallOrMessageForLoadedInteractions:loadedInteractions];
|
||||
if (!firstCallOrMessage) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
BOOL hasTooManyOutgoingMessagesToBlock;
|
||||
if (self.hasTooManyOutgoingMessagesToBlockCached) {
|
||||
hasTooManyOutgoingMessagesToBlock = YES;
|
||||
} else {
|
||||
NSUInteger outgoingMessageCount =
|
||||
[[TSDatabaseView threadOutgoingMessageDatabaseView:transaction] numberOfItemsInGroup:thread.uniqueId];
|
||||
|
||||
const int kMaxBlockOfferOutgoingMessageCount = 10;
|
||||
hasTooManyOutgoingMessagesToBlock = (outgoingMessageCount > kMaxBlockOfferOutgoingMessageCount);
|
||||
self.hasTooManyOutgoingMessagesToBlockCached = hasTooManyOutgoingMessagesToBlock;
|
||||
}
|
||||
|
||||
BOOL shouldHaveBlockOffer = YES;
|
||||
BOOL shouldHaveAddToContactsOffer = YES;
|
||||
BOOL shouldHaveAddToProfileWhitelistOffer = YES;
|
||||
|
||||
NSString *recipientId = ((TSContactThread *)thread).contactIdentifier;
|
||||
|
||||
if ([recipientId isEqualToString:localNumber]) {
|
||||
// Don't add self to contacts.
|
||||
shouldHaveAddToContactsOffer = NO;
|
||||
// Don't bother to block self.
|
||||
shouldHaveBlockOffer = NO;
|
||||
// Don't bother adding self to profile whitelist.
|
||||
shouldHaveAddToProfileWhitelistOffer = NO;
|
||||
} else {
|
||||
if ([[self.blockingManager blockedPhoneNumbers] containsObject:recipientId]) {
|
||||
// Only create "add to contacts" offers for users which are not already blocked.
|
||||
shouldHaveAddToContactsOffer = NO;
|
||||
// Only create block offers for users which are not already blocked.
|
||||
shouldHaveBlockOffer = NO;
|
||||
// Don't create profile whitelist offers for users which are not already blocked.
|
||||
shouldHaveAddToProfileWhitelistOffer = NO;
|
||||
}
|
||||
|
||||
if ([self.contactsManager hasSignalAccountForRecipientId:recipientId]) {
|
||||
// Only create "add to contacts" offers for non-contacts.
|
||||
shouldHaveAddToContactsOffer = NO;
|
||||
// Only create block offers for non-contacts.
|
||||
shouldHaveBlockOffer = NO;
|
||||
// Don't create profile whitelist offers for non-contacts.
|
||||
shouldHaveAddToProfileWhitelistOffer = NO;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasTooManyOutgoingMessagesToBlock) {
|
||||
// If the user has sent more than N messages, don't show a block offer.
|
||||
shouldHaveBlockOffer = NO;
|
||||
}
|
||||
|
||||
BOOL hasOutgoingBeforeIncomingInteraction = [firstCallOrMessage isKindOfClass:[TSOutgoingMessage class]];
|
||||
if ([firstCallOrMessage isKindOfClass:[TSCall class]]) {
|
||||
TSCall *call = (TSCall *)firstCallOrMessage;
|
||||
hasOutgoingBeforeIncomingInteraction
|
||||
= (call.callType == RPRecentCallTypeOutgoing || call.callType == RPRecentCallTypeOutgoingIncomplete);
|
||||
}
|
||||
if (hasOutgoingBeforeIncomingInteraction) {
|
||||
// If there is an outgoing message before an incoming message
|
||||
// the local user initiated this conversation, don't show a block offer.
|
||||
shouldHaveBlockOffer = NO;
|
||||
}
|
||||
|
||||
if (!hasLocalProfile || isThreadInProfileWhitelist) {
|
||||
// Don't show offer if thread is local user hasn't configured their profile.
|
||||
// Don't show offer if thread is already in profile whitelist.
|
||||
shouldHaveAddToProfileWhitelistOffer = NO;
|
||||
} else if (thread.isGroupThread && !hasUnwhitelistedMember) {
|
||||
// Don't show offer in group thread if all members are already individually
|
||||
// whitelisted.
|
||||
shouldHaveAddToProfileWhitelistOffer = NO;
|
||||
}
|
||||
|
||||
BOOL shouldHaveContactOffers
|
||||
= (shouldHaveBlockOffer || shouldHaveAddToContactsOffer || shouldHaveAddToProfileWhitelistOffer);
|
||||
if (!shouldHaveContactOffers) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// We want the offers to be the first interactions in their
|
||||
// conversation's timeline, so we back-date them to slightly before
|
||||
// the first message - or at an arbitrary old timestamp if the
|
||||
// conversation has no messages.
|
||||
uint64_t contactOffersTimestamp = firstCallOrMessage.timestamp - 1;
|
||||
// This view model uses the "unique id" to identify this interaction,
|
||||
// but the interaction is never saved in the database so the specific
|
||||
// value doesn't matter.
|
||||
NSString *uniqueId = @"contact-offers";
|
||||
OWSContactOffersInteraction *offersMessage =
|
||||
[[OWSContactOffersInteraction alloc] initInteractionWithUniqueId:uniqueId
|
||||
timestamp:contactOffersTimestamp
|
||||
thread:thread
|
||||
hasBlockOffer:shouldHaveBlockOffer
|
||||
hasAddToContactsOffer:shouldHaveAddToContactsOffer
|
||||
hasAddToProfileWhitelistOffer:shouldHaveAddToProfileWhitelistOffer
|
||||
recipientId:recipientId
|
||||
beforeInteractionId:firstCallOrMessage.uniqueId];
|
||||
|
||||
OWSLogInfo(@"Creating contact offers: %@ (%llu)", offersMessage.uniqueId, offersMessage.sortId);
|
||||
return offersMessage;
|
||||
}
|
||||
|
||||
// This is a key method. It builds or rebuilds the list of
|
||||
// cell view models.
|
||||
//
|
||||
|
@ -1214,7 +1049,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
|
||||
NSArray<NSString *> *loadedUniqueIds = [self.messageMapping loadedUniqueIds];
|
||||
BOOL isGroupThread = self.thread.isGroupThread;
|
||||
BOOL isRSSFeed = self.isRSSFeed;
|
||||
ConversationStyle *conversationStyle = self.delegate.conversationStyle;
|
||||
|
||||
[self ensureConversationProfileState];
|
||||
|
@ -1228,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];
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
#import "DateUtil.h"
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSFormat.h>
|
||||
#import <SignalUtilitiesKit/NSString+SSK.h>
|
||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface DomainFrontingCountryViewController : OWSViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,98 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "DomainFrontingCountryViewController.h"
|
||||
#import "OWSCountryMetadata.h"
|
||||
#import "OWSTableViewController.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Theme.h>
|
||||
#import <SignalUtilitiesKit/OWSSignalService.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface DomainFrontingCountryViewController ()
|
||||
|
||||
@property (nonatomic, readonly) OWSTableViewController *tableViewController;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation DomainFrontingCountryViewController
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
self.title = NSLocalizedString(
|
||||
@"CENSORSHIP_CIRCUMVENTION_COUNTRY_VIEW_TITLE", @"Title for the 'censorship circumvention country' view.");
|
||||
|
||||
self.view.backgroundColor = Theme.backgroundColor;
|
||||
|
||||
[self createViews];
|
||||
}
|
||||
|
||||
- (void)createViews
|
||||
{
|
||||
_tableViewController = [OWSTableViewController new];
|
||||
[self.view addSubview:self.tableViewController.view];
|
||||
[self.tableViewController.view autoPinEdgeToSuperviewSafeArea:ALEdgeLeading];
|
||||
[self.tableViewController.view autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing];
|
||||
[_tableViewController.view autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.view withOffset:0.0f];
|
||||
[_tableViewController.view autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.view withOffset:0.0f];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - Table Contents
|
||||
|
||||
- (void)updateTableContents
|
||||
{
|
||||
OWSTableContents *contents = [OWSTableContents new];
|
||||
|
||||
NSString *currentCountryCode = OWSSignalService.sharedInstance.manualCensorshipCircumventionCountryCode;
|
||||
|
||||
__weak DomainFrontingCountryViewController *weakSelf = self;
|
||||
|
||||
OWSTableSection *section = [OWSTableSection new];
|
||||
section.headerTitle = NSLocalizedString(
|
||||
@"DOMAIN_FRONTING_COUNTRY_VIEW_SECTION_HEADER", @"Section title for the 'domain fronting country' view.");
|
||||
for (OWSCountryMetadata *countryMetadata in [OWSCountryMetadata allCountryMetadatas]) {
|
||||
[section addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
UITableViewCell *cell = [OWSTableItem newCell];
|
||||
[OWSTableItem configureCell:cell];
|
||||
cell.textLabel.text = countryMetadata.localizedCountryName;
|
||||
|
||||
if ([countryMetadata.countryCode isEqualToString:currentCountryCode]) {
|
||||
cell.accessoryType = UITableViewCellAccessoryCheckmark;
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
actionBlock:^{
|
||||
[weakSelf selectCountry:countryMetadata];
|
||||
}]];
|
||||
}
|
||||
[contents addSection:section];
|
||||
|
||||
self.tableViewController.contents = contents;
|
||||
}
|
||||
|
||||
- (void)selectCountry:(OWSCountryMetadata *)countryMetadata
|
||||
{
|
||||
OWSAssertDebug(countryMetadata);
|
||||
|
||||
OWSSignalService.sharedInstance.manualCensorshipCircumventionCountryCode = countryMetadata.countryCode;
|
||||
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,15 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FingerprintViewController : OWSViewController
|
||||
|
||||
+ (void)presentFromViewController:(UIViewController *)viewController recipientId:(NSString *)recipientId;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,558 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FingerprintViewController.h"
|
||||
#import "FingerprintViewScanController.h"
|
||||
#import "OWSBezierPathView.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/OWSError.h>
|
||||
#import <SignalUtilitiesKit/OWSFingerprint.h>
|
||||
#import <SignalUtilitiesKit/OWSFingerprintBuilder.h>
|
||||
#import <SignalUtilitiesKit/OWSIdentityManager.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+SessionStore.h>
|
||||
#import <SignalUtilitiesKit/TSAccountManager.h>
|
||||
#import <SignalUtilitiesKit/TSInfoMessage.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef void (^CustomLayoutBlock)(void);
|
||||
|
||||
@interface CustomLayoutView : UIView
|
||||
|
||||
@property (nonatomic) CustomLayoutBlock layoutBlock;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation CustomLayoutView
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
if (self = [super initWithCoder:aDecoder]) {
|
||||
self.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
self.layoutBlock();
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface FingerprintViewController () <OWSCompareSafetyNumbersActivityDelegate>
|
||||
|
||||
@property (nonatomic) NSString *recipientId;
|
||||
@property (nonatomic) NSData *identityKey;
|
||||
@property (nonatomic) TSAccountManager *accountManager;
|
||||
@property (nonatomic) OWSFingerprint *fingerprint;
|
||||
@property (nonatomic) NSString *contactName;
|
||||
|
||||
@property (nonatomic) UIBarButtonItem *shareButton;
|
||||
|
||||
@property (nonatomic) UILabel *verificationStateLabel;
|
||||
@property (nonatomic) UILabel *verifyUnverifyButtonLabel;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation FingerprintViewController
|
||||
|
||||
+ (void)presentFromViewController:(UIViewController *)viewController recipientId:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
OWSRecipientIdentity *_Nullable recipientIdentity =
|
||||
[[OWSIdentityManager sharedManager] recipientIdentityForRecipientId:recipientId];
|
||||
if (!recipientIdentity) {
|
||||
[OWSAlerts showAlertWithTitle:NSLocalizedString(@"CANT_VERIFY_IDENTITY_ALERT_TITLE",
|
||||
@"Title for alert explaining that a user cannot be verified.")
|
||||
message:NSLocalizedString(@"CANT_VERIFY_IDENTITY_ALERT_MESSAGE",
|
||||
@"Message for alert explaining that a user cannot be verified.")];
|
||||
return;
|
||||
}
|
||||
|
||||
FingerprintViewController *fingerprintViewController = [FingerprintViewController new];
|
||||
[fingerprintViewController configureWithRecipientId:recipientId];
|
||||
OWSNavigationController *navigationController =
|
||||
[[OWSNavigationController alloc] initWithRootViewController:fingerprintViewController];
|
||||
[viewController presentViewController:navigationController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_accountManager = [TSAccountManager sharedInstance];
|
||||
|
||||
[self observeNotifications];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)observeNotifications
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(identityStateDidChange:)
|
||||
name:kNSNotificationName_IdentityStateDidChange
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)configureWithRecipientId:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
self.recipientId = recipientId;
|
||||
|
||||
OWSContactsManager *contactsManager = Environment.shared.contactsManager;
|
||||
self.contactName = [contactsManager displayNameForPhoneIdentifier:recipientId];
|
||||
|
||||
OWSRecipientIdentity *_Nullable recipientIdentity =
|
||||
[[OWSIdentityManager sharedManager] recipientIdentityForRecipientId:recipientId];
|
||||
OWSAssertDebug(recipientIdentity);
|
||||
// By capturing the identity key when we enter these views, we prevent the edge case
|
||||
// where the user verifies a key that we learned about while this view was open.
|
||||
self.identityKey = recipientIdentity.identityKey;
|
||||
|
||||
OWSFingerprintBuilder *builder =
|
||||
[[OWSFingerprintBuilder alloc] initWithAccountManager:self.accountManager contactsManager:contactsManager];
|
||||
self.fingerprint =
|
||||
[builder fingerprintWithTheirSignalId:recipientId theirIdentityKey:recipientIdentity.identityKey];
|
||||
}
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
self.title = NSLocalizedString(@"PRIVACY_VERIFICATION_TITLE", @"Navbar title");
|
||||
|
||||
self.navigationItem.leftBarButtonItem =
|
||||
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemStop
|
||||
target:self
|
||||
action:@selector(closeButton)
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"stop")];
|
||||
self.shareButton =
|
||||
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction
|
||||
target:self
|
||||
action:@selector(didTapShareButton)
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"share")];
|
||||
self.navigationItem.rightBarButtonItem = self.shareButton;
|
||||
|
||||
[self createViews];
|
||||
}
|
||||
|
||||
- (void)createViews
|
||||
{
|
||||
self.view.backgroundColor = Theme.backgroundColor;
|
||||
|
||||
// Verify/Unverify Button
|
||||
UIView *verifyUnverifyButton = [UIView new];
|
||||
[verifyUnverifyButton
|
||||
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(verifyUnverifyButtonTapped:)]];
|
||||
[self.view addSubview:verifyUnverifyButton];
|
||||
[verifyUnverifyButton autoPinWidthToSuperview];
|
||||
[verifyUnverifyButton autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.view withOffset:0.0f];
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, verifyUnverifyButton);
|
||||
|
||||
UIView *verifyUnverifyPillbox = [UIView new];
|
||||
verifyUnverifyPillbox.backgroundColor = [UIColor ows_materialBlueColor];
|
||||
verifyUnverifyPillbox.layer.cornerRadius = 3.f;
|
||||
verifyUnverifyPillbox.clipsToBounds = YES;
|
||||
[verifyUnverifyButton addSubview:verifyUnverifyPillbox];
|
||||
[verifyUnverifyPillbox autoHCenterInSuperview];
|
||||
[verifyUnverifyPillbox autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:ScaleFromIPhone5To7Plus(10.f, 15.f)];
|
||||
[verifyUnverifyPillbox autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:ScaleFromIPhone5To7Plus(10.f, 20.f)];
|
||||
|
||||
UILabel *verifyUnverifyButtonLabel = [UILabel new];
|
||||
self.verifyUnverifyButtonLabel = verifyUnverifyButtonLabel;
|
||||
verifyUnverifyButtonLabel.font = [UIFont ows_mediumFontWithSize:ScaleFromIPhone5To7Plus(14.f, 20.f)];
|
||||
verifyUnverifyButtonLabel.textColor = [UIColor whiteColor];
|
||||
verifyUnverifyButtonLabel.textAlignment = NSTextAlignmentCenter;
|
||||
[verifyUnverifyPillbox addSubview:verifyUnverifyButtonLabel];
|
||||
[verifyUnverifyButtonLabel autoPinWidthToSuperviewWithMargin:ScaleFromIPhone5To7Plus(50.f, 50.f)];
|
||||
[verifyUnverifyButtonLabel autoPinHeightToSuperviewWithMargin:ScaleFromIPhone5To7Plus(8.f, 8.f)];
|
||||
|
||||
// Learn More
|
||||
UIView *learnMoreButton = [UIView new];
|
||||
[learnMoreButton
|
||||
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(learnMoreButtonTapped:)]];
|
||||
[self.view addSubview:learnMoreButton];
|
||||
[learnMoreButton autoPinWidthToSuperview];
|
||||
[learnMoreButton autoPinEdge:ALEdgeBottom toEdge:ALEdgeTop ofView:verifyUnverifyButton withOffset:0];
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, learnMoreButton);
|
||||
|
||||
UILabel *learnMoreLabel = [UILabel new];
|
||||
learnMoreLabel.attributedText = [[NSAttributedString alloc]
|
||||
initWithString:NSLocalizedString(@"PRIVACY_SAFETY_NUMBERS_LEARN_MORE",
|
||||
@"Label for a link to more information about safety numbers and verification.")
|
||||
attributes:@{
|
||||
NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid),
|
||||
}];
|
||||
learnMoreLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(13.f, 16.f)];
|
||||
learnMoreLabel.textColor = [UIColor ows_materialBlueColor];
|
||||
learnMoreLabel.textAlignment = NSTextAlignmentCenter;
|
||||
[learnMoreButton addSubview:learnMoreLabel];
|
||||
[learnMoreLabel autoPinWidthToSuperviewWithMargin:16.f];
|
||||
[learnMoreLabel autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:ScaleFromIPhone5To7Plus(5.f, 10.f)];
|
||||
[learnMoreLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:ScaleFromIPhone5To7Plus(5.f, 10.f)];
|
||||
|
||||
// Instructions
|
||||
NSString *instructionsFormat = NSLocalizedString(@"PRIVACY_VERIFICATION_INSTRUCTIONS",
|
||||
@"Paragraph(s) shown alongside the safety number when verifying privacy with {{contact name}}");
|
||||
UILabel *instructionsLabel = [UILabel new];
|
||||
instructionsLabel.text = [NSString stringWithFormat:instructionsFormat, self.contactName];
|
||||
instructionsLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 14.f)];
|
||||
instructionsLabel.textColor = Theme.secondaryColor;
|
||||
instructionsLabel.textAlignment = NSTextAlignmentCenter;
|
||||
instructionsLabel.numberOfLines = 0;
|
||||
instructionsLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
[self.view addSubview:instructionsLabel];
|
||||
[instructionsLabel autoPinWidthToSuperviewWithMargin:16.f];
|
||||
[instructionsLabel autoPinEdge:ALEdgeBottom toEdge:ALEdgeTop ofView:learnMoreButton withOffset:0];
|
||||
|
||||
// Fingerprint Label
|
||||
UILabel *fingerprintLabel = [UILabel new];
|
||||
fingerprintLabel.text = self.fingerprint.displayableText;
|
||||
fingerprintLabel.font = [UIFont fontWithName:@"Menlo-Regular" size:ScaleFromIPhone5To7Plus(20.f, 23.f)];
|
||||
fingerprintLabel.textColor = Theme.secondaryColor;
|
||||
fingerprintLabel.numberOfLines = 3;
|
||||
fingerprintLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
fingerprintLabel.adjustsFontSizeToFitWidth = YES;
|
||||
[fingerprintLabel
|
||||
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(fingerprintLabelTapped:)]];
|
||||
fingerprintLabel.userInteractionEnabled = YES;
|
||||
[self.view addSubview:fingerprintLabel];
|
||||
[fingerprintLabel autoPinWidthToSuperviewWithMargin:ScaleFromIPhone5To7Plus(50.f, 60.f)];
|
||||
[fingerprintLabel autoPinEdge:ALEdgeBottom
|
||||
toEdge:ALEdgeTop
|
||||
ofView:instructionsLabel
|
||||
withOffset:-ScaleFromIPhone5To7Plus(8.f, 15.f)];
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, fingerprintLabel);
|
||||
|
||||
// Fingerprint Image
|
||||
CustomLayoutView *fingerprintView = [CustomLayoutView new];
|
||||
[self.view addSubview:fingerprintView];
|
||||
[fingerprintView autoPinWidthToSuperview];
|
||||
[fingerprintView autoPinEdge:ALEdgeBottom
|
||||
toEdge:ALEdgeTop
|
||||
ofView:fingerprintLabel
|
||||
withOffset:-ScaleFromIPhone5To7Plus(10.f, 15.f)];
|
||||
[fingerprintView
|
||||
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(fingerprintViewTapped:)]];
|
||||
fingerprintView.userInteractionEnabled = YES;
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, fingerprintView);
|
||||
|
||||
OWSBezierPathView *fingerprintCircle = [OWSBezierPathView new];
|
||||
[fingerprintCircle setConfigureShapeLayerBlock:^(CAShapeLayer *layer, CGRect bounds) {
|
||||
layer.fillColor = Theme.offBackgroundColor.CGColor;
|
||||
CGFloat size = MIN(bounds.size.width, bounds.size.height);
|
||||
CGRect circle = CGRectMake((bounds.size.width - size) * 0.5f, (bounds.size.height - size) * 0.5f, size, size);
|
||||
layer.path = [UIBezierPath bezierPathWithOvalInRect:circle].CGPath;
|
||||
}];
|
||||
[fingerprintView addSubview:fingerprintCircle];
|
||||
[fingerprintCircle ows_autoPinToSuperviewEdges];
|
||||
|
||||
UIImageView *fingerprintImageView = [UIImageView new];
|
||||
fingerprintImageView.image = self.fingerprint.image;
|
||||
// Don't antialias QR Codes.
|
||||
fingerprintImageView.layer.magnificationFilter = kCAFilterNearest;
|
||||
fingerprintImageView.layer.minificationFilter = kCAFilterNearest;
|
||||
[fingerprintView addSubview:fingerprintImageView];
|
||||
|
||||
UILabel *scanLabel = [UILabel new];
|
||||
scanLabel.text = NSLocalizedString(@"PRIVACY_TAP_TO_SCAN", @"Button that shows the 'scan with camera' view.");
|
||||
scanLabel.font = [UIFont ows_mediumFontWithSize:ScaleFromIPhone5To7Plus(14.f, 16.f)];
|
||||
scanLabel.textColor = Theme.secondaryColor;
|
||||
[scanLabel sizeToFit];
|
||||
[fingerprintView addSubview:scanLabel];
|
||||
|
||||
fingerprintView.layoutBlock = ^{
|
||||
CGFloat size = round(MIN(fingerprintView.width, fingerprintView.height) * 0.675f);
|
||||
fingerprintImageView.frame = CGRectMake(
|
||||
round((fingerprintView.width - size) * 0.5f), round((fingerprintView.height - size) * 0.5f), size, size);
|
||||
CGFloat scanY = round(fingerprintImageView.bottom
|
||||
+ ((fingerprintView.height - fingerprintImageView.bottom) - scanLabel.height) * 0.33f);
|
||||
scanLabel.frame = CGRectMake(
|
||||
round((fingerprintView.width - scanLabel.width) * 0.5f), scanY, scanLabel.width, scanLabel.height);
|
||||
};
|
||||
|
||||
// Verification State
|
||||
UILabel *verificationStateLabel = [UILabel new];
|
||||
self.verificationStateLabel = verificationStateLabel;
|
||||
verificationStateLabel.font = [UIFont ows_mediumFontWithSize:ScaleFromIPhone5To7Plus(16.f, 20.f)];
|
||||
verificationStateLabel.textColor = Theme.secondaryColor;
|
||||
verificationStateLabel.textAlignment = NSTextAlignmentCenter;
|
||||
verificationStateLabel.numberOfLines = 0;
|
||||
verificationStateLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
[self.view addSubview:verificationStateLabel];
|
||||
[verificationStateLabel autoPinWidthToSuperviewWithMargin:16.f];
|
||||
// Bind height of label to height of two lines of text.
|
||||
// This should always be sufficient, and will prevent the view's
|
||||
// layout from changing if the user is marked as verified or not
|
||||
// verified.
|
||||
[verificationStateLabel autoSetDimension:ALDimensionHeight
|
||||
toSize:round(verificationStateLabel.font.lineHeight * 2.25f)];
|
||||
[verificationStateLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.view withOffset:ScaleFromIPhone5To7Plus(15.f, 20.f)];
|
||||
[verificationStateLabel autoPinEdge:ALEdgeBottom
|
||||
toEdge:ALEdgeTop
|
||||
ofView:fingerprintView
|
||||
withOffset:-ScaleFromIPhone5To7Plus(10.f, 15.f)];
|
||||
|
||||
[self updateVerificationStateLabel];
|
||||
}
|
||||
|
||||
- (void)updateVerificationStateLabel
|
||||
{
|
||||
OWSAssertDebug(self.recipientId.length > 0);
|
||||
|
||||
BOOL isVerified = [[OWSIdentityManager sharedManager] verificationStateForRecipientId:self.recipientId]
|
||||
== OWSVerificationStateVerified;
|
||||
|
||||
if (isVerified) {
|
||||
NSMutableAttributedString *labelText = [NSMutableAttributedString new];
|
||||
|
||||
if (isVerified) {
|
||||
// Show a "checkmark" if this user is verified.
|
||||
[labelText
|
||||
appendAttributedString:[[NSAttributedString alloc]
|
||||
initWithString:LocalizationNotNeeded(@"\uf00c ")
|
||||
attributes:@{
|
||||
NSFontAttributeName : [UIFont
|
||||
ows_fontAwesomeFont:self.verificationStateLabel.font.pointSize],
|
||||
}]];
|
||||
}
|
||||
|
||||
[labelText
|
||||
appendAttributedString:
|
||||
[[NSAttributedString alloc]
|
||||
initWithString:[NSString stringWithFormat:NSLocalizedString(@"PRIVACY_IDENTITY_IS_VERIFIED_FORMAT",
|
||||
@"Label indicating that the user is verified. Embeds "
|
||||
@"{{the user's name or phone number}}."),
|
||||
self.contactName]]];
|
||||
self.verificationStateLabel.attributedText = labelText;
|
||||
|
||||
self.verifyUnverifyButtonLabel.text = NSLocalizedString(
|
||||
@"PRIVACY_UNVERIFY_BUTTON", @"Button that lets user mark another user's identity as unverified.");
|
||||
} else {
|
||||
self.verificationStateLabel.text = [NSString
|
||||
stringWithFormat:NSLocalizedString(@"PRIVACY_IDENTITY_IS_NOT_VERIFIED_FORMAT",
|
||||
@"Label indicating that the user is not verified. Embeds {{the user's name or phone "
|
||||
@"number}}."),
|
||||
self.contactName];
|
||||
|
||||
NSMutableAttributedString *buttonText = [NSMutableAttributedString new];
|
||||
// Show a "checkmark" if this user is not verified.
|
||||
[buttonText
|
||||
appendAttributedString:[[NSAttributedString alloc]
|
||||
initWithString:LocalizationNotNeeded(@"\uf00c ")
|
||||
attributes:@{
|
||||
NSFontAttributeName : [UIFont
|
||||
ows_fontAwesomeFont:self.verifyUnverifyButtonLabel.font.pointSize],
|
||||
}]];
|
||||
[buttonText appendAttributedString:
|
||||
[[NSAttributedString alloc]
|
||||
initWithString:NSLocalizedString(@"PRIVACY_VERIFY_BUTTON",
|
||||
@"Button that lets user mark another user's identity as verified.")]];
|
||||
self.verifyUnverifyButtonLabel.attributedText = buttonText;
|
||||
}
|
||||
|
||||
[self.view setNeedsLayout];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)showSharingActivityWithCompletion:(nullable void (^)(void))completionHandler
|
||||
{
|
||||
OWSLogDebug(@"Sharing safety numbers");
|
||||
|
||||
OWSCompareSafetyNumbersActivity *compareActivity = [[OWSCompareSafetyNumbersActivity alloc] initWithDelegate:self];
|
||||
|
||||
NSString *shareFormat = NSLocalizedString(
|
||||
@"SAFETY_NUMBER_SHARE_FORMAT", @"Snippet to share {{safety number}} with a friend. sent e.g. via SMS");
|
||||
NSString *shareString = [NSString stringWithFormat:shareFormat, self.fingerprint.displayableText];
|
||||
|
||||
UIActivityViewController *activityController =
|
||||
[[UIActivityViewController alloc] initWithActivityItems:@[ shareString ]
|
||||
applicationActivities:@[ compareActivity ]];
|
||||
|
||||
activityController.completionWithItemsHandler = ^void(UIActivityType __nullable activityType,
|
||||
BOOL completed,
|
||||
NSArray *__nullable returnedItems,
|
||||
NSError *__nullable activityError) {
|
||||
if (completionHandler) {
|
||||
completionHandler();
|
||||
}
|
||||
};
|
||||
|
||||
// This value was extracted by inspecting `activityType` in the activityController.completionHandler
|
||||
NSString *const iCloudActivityType = @"com.apple.CloudDocsUI.AddToiCloudDrive";
|
||||
activityController.excludedActivityTypes = @[
|
||||
UIActivityTypePostToFacebook,
|
||||
UIActivityTypePostToWeibo,
|
||||
UIActivityTypeAirDrop,
|
||||
UIActivityTypePostToTwitter,
|
||||
iCloudActivityType // This isn't being excluded. RADAR https://openradar.appspot.com/27493621
|
||||
];
|
||||
|
||||
[self presentViewController:activityController animated:YES completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - OWSCompareSafetyNumbersActivityDelegate
|
||||
|
||||
- (void)compareSafetyNumbersActivitySucceededWithActivity:(OWSCompareSafetyNumbersActivity *)activity
|
||||
{
|
||||
[self showVerificationSucceeded];
|
||||
}
|
||||
|
||||
- (void)compareSafetyNumbersActivity:(OWSCompareSafetyNumbersActivity *)activity failedWithError:(NSError *)error
|
||||
{
|
||||
[self showVerificationFailedWithError:error];
|
||||
}
|
||||
|
||||
- (void)showVerificationSucceeded
|
||||
{
|
||||
[FingerprintViewScanController showVerificationSucceeded:self
|
||||
identityKey:self.identityKey
|
||||
recipientId:self.recipientId
|
||||
contactName:self.contactName
|
||||
tag:self.logTag];
|
||||
}
|
||||
|
||||
- (void)showVerificationFailedWithError:(NSError *)error
|
||||
{
|
||||
|
||||
[FingerprintViewScanController showVerificationFailedWithError:error
|
||||
viewController:self
|
||||
retryBlock:nil
|
||||
cancelBlock:^{
|
||||
// Do nothing.
|
||||
}
|
||||
tag:self.logTag];
|
||||
}
|
||||
|
||||
#pragma mark - Action
|
||||
|
||||
- (void)closeButton
|
||||
{
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)didTapShareButton
|
||||
{
|
||||
[self showSharingActivityWithCompletion:nil];
|
||||
}
|
||||
|
||||
- (void)showScanner
|
||||
{
|
||||
FingerprintViewScanController *scanView = [FingerprintViewScanController new];
|
||||
[scanView configureWithRecipientId:self.recipientId];
|
||||
[self.navigationController pushViewController:scanView animated:YES];
|
||||
}
|
||||
|
||||
- (void)learnMoreButtonTapped:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateRecognized) {
|
||||
NSString *learnMoreURL = @"https://support.signal.org/hc/en-us/articles/"
|
||||
@"213134107";
|
||||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:learnMoreURL]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)fingerprintLabelTapped:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateRecognized) {
|
||||
[self showSharingActivityWithCompletion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)fingerprintViewTapped:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateRecognized) {
|
||||
[self showScanner];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)verifyUnverifyButtonTapped:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
if (gestureRecognizer.state == UIGestureRecognizerStateRecognized) {
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
BOOL isVerified = [[OWSIdentityManager sharedManager] verificationStateForRecipientId:self.recipientId
|
||||
transaction:transaction]
|
||||
== OWSVerificationStateVerified;
|
||||
|
||||
OWSVerificationState newVerificationState
|
||||
= (isVerified ? OWSVerificationStateDefault : OWSVerificationStateVerified);
|
||||
[[OWSIdentityManager sharedManager] setVerificationState:newVerificationState
|
||||
identityKey:self.identityKey
|
||||
recipientId:self.recipientId
|
||||
isUserInitiatedChange:YES
|
||||
transaction:transaction];
|
||||
}];
|
||||
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)identityStateDidChange:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self updateVerificationStateLabel];
|
||||
}
|
||||
|
||||
#pragma mark - Orientation
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
|
||||
{
|
||||
return UIInterfaceOrientationMaskPortrait;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FingerprintViewScanController : OWSViewController
|
||||
|
||||
- (void)configureWithRecipientId:(NSString *)recipientId NS_SWIFT_NAME(configure(recipientId:));
|
||||
|
||||
+ (void)showVerificationSucceeded:(UIViewController *)viewController
|
||||
identityKey:(NSData *)identityKey
|
||||
recipientId:(NSString *)recipientId
|
||||
contactName:(NSString *)contactName
|
||||
tag:(NSString *)tag;
|
||||
|
||||
+ (void)showVerificationFailedWithError:(NSError *)error
|
||||
viewController:(UIViewController *)viewController
|
||||
retryBlock:(void (^_Nullable)(void))retryBlock
|
||||
cancelBlock:(void (^_Nonnull)(void))cancelBlock
|
||||
tag:(NSString *)tag;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,258 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FingerprintViewScanController.h"
|
||||
#import "OWSQRCodeScanningViewController.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import "UIViewController+Permissions.h"
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSContactsManager.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/OWSError.h>
|
||||
#import <SignalUtilitiesKit/OWSFingerprint.h>
|
||||
#import <SignalUtilitiesKit/OWSFingerprintBuilder.h>
|
||||
#import <SignalUtilitiesKit/OWSIdentityManager.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FingerprintViewScanController () <OWSQRScannerDelegate>
|
||||
|
||||
@property (nonatomic) TSAccountManager *accountManager;
|
||||
@property (nonatomic) NSString *recipientId;
|
||||
@property (nonatomic) NSData *identityKey;
|
||||
@property (nonatomic) OWSFingerprint *fingerprint;
|
||||
@property (nonatomic) NSString *contactName;
|
||||
@property (nonatomic) OWSQRCodeScanningViewController *qrScanningController;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation FingerprintViewScanController
|
||||
|
||||
- (void)configureWithRecipientId:(NSString *)recipientId
|
||||
{
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
|
||||
self.recipientId = recipientId;
|
||||
self.accountManager = [TSAccountManager sharedInstance];
|
||||
|
||||
OWSContactsManager *contactsManager = Environment.shared.contactsManager;
|
||||
self.contactName = [contactsManager displayNameForPhoneIdentifier:recipientId];
|
||||
|
||||
OWSRecipientIdentity *_Nullable recipientIdentity =
|
||||
[[OWSIdentityManager sharedManager] recipientIdentityForRecipientId:recipientId];
|
||||
OWSAssertDebug(recipientIdentity);
|
||||
// By capturing the identity key when we enter these views, we prevent the edge case
|
||||
// where the user verifies a key that we learned about while this view was open.
|
||||
self.identityKey = recipientIdentity.identityKey;
|
||||
|
||||
OWSFingerprintBuilder *builder =
|
||||
[[OWSFingerprintBuilder alloc] initWithAccountManager:self.accountManager contactsManager:contactsManager];
|
||||
self.fingerprint =
|
||||
[builder fingerprintWithTheirSignalId:recipientId theirIdentityKey:recipientIdentity.identityKey];
|
||||
}
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
self.title = NSLocalizedString(@"SCAN_QR_CODE_VIEW_TITLE", @"Title for the 'scan QR code' view.");
|
||||
|
||||
[self createViews];
|
||||
}
|
||||
|
||||
- (void)createViews
|
||||
{
|
||||
self.view.backgroundColor = UIColor.blackColor;
|
||||
|
||||
self.qrScanningController = [OWSQRCodeScanningViewController new];
|
||||
self.qrScanningController.scanDelegate = self;
|
||||
[self.view addSubview:self.qrScanningController.view];
|
||||
[self.qrScanningController.view autoPinWidthToSuperview];
|
||||
[self.qrScanningController.view autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.view withOffset:0.0f];
|
||||
|
||||
UIView *footer = [UIView new];
|
||||
footer.backgroundColor = [UIColor colorWithWhite:0.25f alpha:1.f];
|
||||
[self.view addSubview:footer];
|
||||
[footer autoPinWidthToSuperview];
|
||||
[footer autoPinEdgeToSuperviewEdge:ALEdgeBottom];
|
||||
[footer autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.qrScanningController.view];
|
||||
|
||||
UILabel *cameraInstructionLabel = [UILabel new];
|
||||
cameraInstructionLabel.text
|
||||
= NSLocalizedString(@"SCAN_CODE_INSTRUCTIONS", @"label presented once scanning (camera) view is visible.");
|
||||
cameraInstructionLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(14.f, 18.f)];
|
||||
cameraInstructionLabel.textColor = [UIColor whiteColor];
|
||||
cameraInstructionLabel.textAlignment = NSTextAlignmentCenter;
|
||||
cameraInstructionLabel.numberOfLines = 0;
|
||||
cameraInstructionLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
[footer addSubview:cameraInstructionLabel];
|
||||
[cameraInstructionLabel autoPinWidthToSuperviewWithMargin:ScaleFromIPhone5To7Plus(16.f, 30.f)];
|
||||
CGFloat instructionsVMargin = ScaleFromIPhone5To7Plus(10.f, 20.f);
|
||||
[cameraInstructionLabel autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.view withOffset:instructionsVMargin];
|
||||
[cameraInstructionLabel autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:instructionsVMargin];
|
||||
}
|
||||
|
||||
#pragma mark - Action
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
[self ows_askForCameraPermissions:^(BOOL granted) {
|
||||
if (granted) {
|
||||
// Camera stops capturing when "sharing" while in capture mode.
|
||||
// Also, it's less obvious whats being "shared" at this point,
|
||||
// so just disable sharing when in capture mode.
|
||||
|
||||
OWSLogInfo(@"Showing Scanner");
|
||||
|
||||
[self.qrScanningController startCapture];
|
||||
} else {
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - OWSQRScannerDelegate
|
||||
|
||||
- (void)controller:(OWSQRCodeScanningViewController *)controller didDetectQRCodeWithData:(NSData *)data
|
||||
{
|
||||
[self verifyCombinedFingerprintData:data];
|
||||
}
|
||||
|
||||
- (void)verifyCombinedFingerprintData:(NSData *)combinedFingerprintData
|
||||
{
|
||||
NSError *error;
|
||||
if ([self.fingerprint matchesLogicalFingerprintsData:combinedFingerprintData error:&error]) {
|
||||
[self showVerificationSucceeded];
|
||||
} else {
|
||||
[self showVerificationFailedWithError:error];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showVerificationSucceeded
|
||||
{
|
||||
[self.class showVerificationSucceeded:self
|
||||
identityKey:self.identityKey
|
||||
recipientId:self.recipientId
|
||||
contactName:self.contactName
|
||||
tag:self.logTag];
|
||||
}
|
||||
|
||||
- (void)showVerificationFailedWithError:(NSError *)error
|
||||
{
|
||||
|
||||
[self.class showVerificationFailedWithError:error
|
||||
viewController:self
|
||||
retryBlock:^{
|
||||
[self.qrScanningController startCapture];
|
||||
}
|
||||
cancelBlock:^{
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
tag:self.logTag];
|
||||
}
|
||||
|
||||
+ (void)showVerificationSucceeded:(UIViewController *)viewController
|
||||
identityKey:(NSData *)identityKey
|
||||
recipientId:(NSString *)recipientId
|
||||
contactName:(NSString *)contactName
|
||||
tag:(NSString *)tag
|
||||
{
|
||||
OWSAssertDebug(viewController);
|
||||
OWSAssertDebug(identityKey.length > 0);
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
OWSAssertDebug(contactName.length > 0);
|
||||
OWSAssertDebug(tag.length > 0);
|
||||
|
||||
OWSLogInfo(@"%@ Successfully verified safety numbers.", tag);
|
||||
|
||||
NSString *successTitle = NSLocalizedString(@"SUCCESSFUL_VERIFICATION_TITLE", nil);
|
||||
NSString *descriptionFormat = NSLocalizedString(
|
||||
@"SUCCESSFUL_VERIFICATION_DESCRIPTION", @"Alert body after verifying privacy with {{other user's name}}");
|
||||
NSString *successDescription = [NSString stringWithFormat:descriptionFormat, contactName];
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:successTitle
|
||||
message:successDescription
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert
|
||||
addAction:[UIAlertAction
|
||||
actionWithTitle:NSLocalizedString(@"FINGERPRINT_SCAN_VERIFY_BUTTON",
|
||||
@"Button that marks user as verified after a successful fingerprint scan.")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[OWSIdentityManager.sharedManager setVerificationState:OWSVerificationStateVerified
|
||||
identityKey:identityKey
|
||||
recipientId:recipientId
|
||||
isUserInitiatedChange:YES];
|
||||
[viewController dismissViewControllerAnimated:true completion:nil];
|
||||
}]];
|
||||
UIAlertAction *dismissAction =
|
||||
[UIAlertAction actionWithTitle:CommonStrings.dismissButton
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[viewController dismissViewControllerAnimated:true completion:nil];
|
||||
}];
|
||||
[alert addAction:dismissAction];
|
||||
|
||||
[viewController presentAlert:alert];
|
||||
}
|
||||
|
||||
+ (void)showVerificationFailedWithError:(NSError *)error
|
||||
viewController:(UIViewController *)viewController
|
||||
retryBlock:(void (^_Nullable)(void))retryBlock
|
||||
cancelBlock:(void (^_Nonnull)(void))cancelBlock
|
||||
tag:(NSString *)tag
|
||||
{
|
||||
OWSAssertDebug(viewController);
|
||||
OWSAssertDebug(cancelBlock);
|
||||
OWSAssertDebug(tag.length > 0);
|
||||
|
||||
OWSLogInfo(@"%@ Failed to verify safety numbers.", tag);
|
||||
|
||||
NSString *_Nullable failureTitle;
|
||||
if (error.code != OWSErrorCodeUserError) {
|
||||
failureTitle = NSLocalizedString(@"FAILED_VERIFICATION_TITLE", @"alert title");
|
||||
} // else no title. We don't want to show a big scary "VERIFICATION FAILED" when it's just user error.
|
||||
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:failureTitle
|
||||
message:error.localizedDescription
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
if (retryBlock) {
|
||||
[alert addAction:[UIAlertAction actionWithTitle:[CommonStrings retryButton]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
retryBlock();
|
||||
}]];
|
||||
}
|
||||
|
||||
[alert addAction:[OWSAlerts cancelAction]];
|
||||
|
||||
[viewController presentAlert:alert];
|
||||
|
||||
OWSLogWarn(@"%@ Identity verification failed with error: %@", tag, error);
|
||||
}
|
||||
|
||||
- (void)dismissViewControllerAnimated:(BOOL)animated completion:(nullable void (^)(void))completion
|
||||
{
|
||||
self.qrScanningController.view.hidden = YES;
|
||||
|
||||
[super dismissViewControllerAnimated:animated completion:completion];
|
||||
}
|
||||
|
||||
#pragma mark - Orientation
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
|
||||
{
|
||||
return UIInterfaceOrientationMaskPortrait;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -35,7 +35,6 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
|
|||
public weak var delegate: GifPickerViewControllerDelegate?
|
||||
|
||||
let thread: TSThread
|
||||
let messageSender: MessageSender
|
||||
|
||||
let searchBar: SearchBar
|
||||
let layout: GifPickerLayout
|
||||
|
@ -60,9 +59,8 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
|
|||
}
|
||||
|
||||
@objc
|
||||
required init(thread: TSThread, messageSender: MessageSender) {
|
||||
required init(thread: TSThread) {
|
||||
self.thread = thread
|
||||
self.messageSender = messageSender
|
||||
|
||||
self.searchBar = SearchBar()
|
||||
self.layout = GifPickerLayout()
|
||||
|
@ -390,7 +388,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
|
|||
owsFailDebug("couldn't load asset.")
|
||||
return
|
||||
}
|
||||
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: rendition.utiType, imageQuality: .original)
|
||||
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: rendition.utiType, imageQuality: .medium)
|
||||
|
||||
strongSelf.dismiss(animated: true) {
|
||||
// Delegate presents view controllers, so it's important that *this* controller be dismissed before that occurs.
|
||||
|
|
|
@ -7,15 +7,9 @@ import SignalUtilitiesKit
|
|||
|
||||
@objc class GroupTableViewCell: UITableViewCell {
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private var contactsManager: OWSContactsManager {
|
||||
return Environment.shared.contactsManager
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private let avatarView = AvatarImageView()
|
||||
// private let avatarView = AvatarImageView()
|
||||
private let nameLabel = UILabel()
|
||||
private let subtitleLabel = UILabel()
|
||||
|
||||
|
@ -30,14 +24,14 @@ import SignalUtilitiesKit
|
|||
|
||||
// Layout
|
||||
|
||||
avatarView.autoSetDimension(.width, toSize: CGFloat(kStandardAvatarSize))
|
||||
avatarView.autoPinToSquareAspectRatio()
|
||||
// avatarView.autoSetDimension(.width, toSize: CGFloat(kStandardAvatarSize))
|
||||
// avatarView.autoPinToSquareAspectRatio()
|
||||
|
||||
let textRows = UIStackView(arrangedSubviews: [nameLabel, subtitleLabel])
|
||||
textRows.axis = .vertical
|
||||
textRows.alignment = .leading
|
||||
|
||||
let columns = UIStackView(arrangedSubviews: [avatarView, textRows])
|
||||
let columns = UIStackView(arrangedSubviews: [ textRows ])
|
||||
columns.axis = .horizontal
|
||||
columns.alignment = .center
|
||||
columns.spacing = kContactCellAvatarTextMargin
|
||||
|
@ -62,11 +56,11 @@ import SignalUtilitiesKit
|
|||
|
||||
let groupMemberIds: [String] = thread.groupModel.groupMemberIds
|
||||
let groupMemberNames = groupMemberIds.map { (recipientId: String) in
|
||||
contactsManager.displayName(forPhoneIdentifier: recipientId)
|
||||
SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: recipientId, avoidingWriteTransaction: true)!
|
||||
}.joined(separator: ", ")
|
||||
self.subtitleLabel.text = groupMemberNames
|
||||
|
||||
self.avatarView.image = OWSAvatarBuilder.buildImage(thread: thread, diameter: kStandardAvatarSize)
|
||||
// self.avatarView.image = OWSAvatarBuilder.buildImage(thread: thread, diameter: kStandardAvatarSize)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,271 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Social
|
||||
import ContactsUI
|
||||
import MessageUI
|
||||
import SignalUtilitiesKit
|
||||
|
||||
@objc(OWSInviteFlow)
|
||||
class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailComposeViewControllerDelegate, ContactsPickerDelegate {
|
||||
enum Channel {
|
||||
case message, mail, twitter
|
||||
}
|
||||
|
||||
let installUrl = "https://signal.org/install/"
|
||||
let homepageUrl = "https://signal.org"
|
||||
|
||||
@objc
|
||||
let actionSheetController: UIAlertController
|
||||
|
||||
@objc
|
||||
let presentingViewController: UIViewController
|
||||
|
||||
var channel: Channel?
|
||||
|
||||
@objc
|
||||
required init(presentingViewController: UIViewController) {
|
||||
self.presentingViewController = presentingViewController
|
||||
actionSheetController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
super.init()
|
||||
|
||||
actionSheetController.addAction(dismissAction())
|
||||
|
||||
if let messageAction = messageAction() {
|
||||
actionSheetController.addAction(messageAction)
|
||||
}
|
||||
|
||||
if let mailAction = mailAction() {
|
||||
actionSheetController.addAction(mailAction)
|
||||
}
|
||||
|
||||
if let tweetAction = tweetAction() {
|
||||
actionSheetController.addAction(tweetAction)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
Logger.verbose("[InviteFlow] deinit")
|
||||
}
|
||||
|
||||
// MARK: Twitter
|
||||
|
||||
func canTweet() -> Bool {
|
||||
return SLComposeViewController.isAvailable(forServiceType: SLServiceTypeTwitter)
|
||||
}
|
||||
|
||||
func tweetAction() -> UIAlertAction? {
|
||||
guard canTweet() else {
|
||||
Logger.info("Twitter not supported.")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let twitterViewController = SLComposeViewController(forServiceType: SLServiceTypeTwitter) else {
|
||||
Logger.error("unable to build twitter controller.")
|
||||
return nil
|
||||
}
|
||||
|
||||
let tweetString = NSLocalizedString("SETTINGS_INVITE_TWITTER_TEXT", comment: "content of tweet when inviting via twitter - please do not translate URL")
|
||||
twitterViewController.setInitialText(tweetString)
|
||||
|
||||
let tweetUrl = URL(string: installUrl)
|
||||
twitterViewController.add(tweetUrl)
|
||||
twitterViewController.add(#imageLiteral(resourceName: "twitter_sharing_image"))
|
||||
|
||||
let tweetTitle = NSLocalizedString("SHARE_ACTION_TWEET", comment: "action sheet item")
|
||||
return UIAlertAction(title: tweetTitle, style: .default) { _ in
|
||||
Logger.debug("Chose tweet")
|
||||
|
||||
self.presentingViewController.present(twitterViewController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func dismissAction() -> UIAlertAction {
|
||||
return UIAlertAction(title: CommonStrings.dismissButton, style: .cancel)
|
||||
}
|
||||
|
||||
// MARK: ContactsPickerDelegate
|
||||
|
||||
func contactsPicker(_: ContactsPicker, didSelectMultipleContacts contacts: [Contact]) {
|
||||
Logger.debug("didSelectContacts:\(contacts)")
|
||||
|
||||
guard let inviteChannel = channel else {
|
||||
Logger.error("unexpected nil channel after returning from contact picker.")
|
||||
self.presentingViewController.dismiss(animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
switch inviteChannel {
|
||||
case .message:
|
||||
let phoneNumbers: [String] = contacts.map { $0.userTextPhoneNumbers.first }.filter { $0 != nil }.map { $0! }
|
||||
dismissAndSendSMSTo(phoneNumbers: phoneNumbers)
|
||||
case .mail:
|
||||
let recipients: [String] = contacts.map { $0.emails.first }.filter { $0 != nil }.map { $0! }
|
||||
sendMailTo(emails: recipients)
|
||||
default:
|
||||
Logger.error("unexpected channel after returning from contact picker: \(inviteChannel)")
|
||||
}
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, shouldSelectContact contact: Contact) -> Bool {
|
||||
guard let inviteChannel = channel else {
|
||||
Logger.error("unexpected nil channel in contact picker.")
|
||||
return true
|
||||
}
|
||||
|
||||
switch inviteChannel {
|
||||
case .message:
|
||||
return contact.userTextPhoneNumbers.count > 0
|
||||
case .mail:
|
||||
return contact.emails.count > 0
|
||||
default:
|
||||
Logger.error("unexpected channel after returning from contact picker: \(inviteChannel)")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, contactFetchDidFail error: NSError) {
|
||||
Logger.error("with error: \(error)")
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
OWSAlerts.showErrorAlert(message: NSLocalizedString("ERROR_COULD_NOT_FETCH_CONTACTS", comment: "Error indicating that the phone's contacts could not be retrieved."))
|
||||
}
|
||||
}
|
||||
|
||||
func contactsPickerDidCancel(_: ContactsPicker) {
|
||||
Logger.debug("")
|
||||
self.presentingViewController.dismiss(animated: true)
|
||||
}
|
||||
|
||||
func contactsPicker(_: ContactsPicker, didSelectContact contact: Contact) {
|
||||
owsFailDebug("InviteFlow only supports multi-select")
|
||||
self.presentingViewController.dismiss(animated: true)
|
||||
}
|
||||
|
||||
// MARK: SMS
|
||||
|
||||
func messageAction() -> UIAlertAction? {
|
||||
guard MFMessageComposeViewController.canSendText() else {
|
||||
Logger.info("Device cannot send text")
|
||||
return nil
|
||||
}
|
||||
|
||||
let messageTitle = NSLocalizedString("SHARE_ACTION_MESSAGE", comment: "action sheet item to open native messages app")
|
||||
return UIAlertAction(title: messageTitle, style: .default) { _ in
|
||||
Logger.debug("Chose message.")
|
||||
self.channel = .message
|
||||
let picker = ContactsPicker(allowsMultipleSelection: true, subtitleCellType: .phoneNumber)
|
||||
picker.contactsPickerDelegate = self
|
||||
picker.title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
|
||||
let navigationController = OWSNavigationController(rootViewController: picker)
|
||||
self.presentingViewController.present(navigationController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
public func dismissAndSendSMSTo(phoneNumbers: [String]) {
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
if phoneNumbers.count > 1 {
|
||||
let warning = UIAlertController(title: nil,
|
||||
message: NSLocalizedString("INVITE_WARNING_MULTIPLE_INVITES_BY_TEXT",
|
||||
comment: "Alert warning that sending an invite to multiple users will create a group message whose recipients will be able to see each other."),
|
||||
preferredStyle: .alert)
|
||||
warning.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_CONTINUE",
|
||||
comment: "Label for 'continue' button."),
|
||||
style: .default, handler: { _ in
|
||||
self.sendSMSTo(phoneNumbers: phoneNumbers)
|
||||
}))
|
||||
warning.addAction(OWSAlerts.cancelAction)
|
||||
self.presentingViewController.presentAlert(warning)
|
||||
} else {
|
||||
self.sendSMSTo(phoneNumbers: phoneNumbers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public func sendSMSTo(phoneNumbers: [String]) {
|
||||
let messageComposeViewController = MFMessageComposeViewController()
|
||||
messageComposeViewController.messageComposeDelegate = self
|
||||
messageComposeViewController.recipients = phoneNumbers
|
||||
|
||||
let inviteText = NSLocalizedString("SMS_INVITE_BODY", comment: "body sent to contacts when inviting to Install Signal")
|
||||
messageComposeViewController.body = inviteText.appending(" \(self.installUrl)")
|
||||
self.presentingViewController.present(messageComposeViewController, animated: true)
|
||||
}
|
||||
|
||||
// MARK: MessageComposeViewControllerDelegate
|
||||
|
||||
func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
switch result {
|
||||
case .failed:
|
||||
let warning = UIAlertController(title: nil, message: NSLocalizedString("SEND_INVITE_FAILURE", comment: "Alert body after invite failed"), preferredStyle: .alert)
|
||||
warning.addAction(UIAlertAction(title: CommonStrings.dismissButton, style: .default, handler: nil))
|
||||
self.presentingViewController.present(warning, animated: true, completion: nil)
|
||||
case .sent:
|
||||
Logger.debug("user successfully invited their friends via SMS.")
|
||||
case .cancelled:
|
||||
Logger.debug("user cancelled message invite")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Mail
|
||||
|
||||
func mailAction() -> UIAlertAction? {
|
||||
guard MFMailComposeViewController.canSendMail() else {
|
||||
Logger.info("Device cannot send mail")
|
||||
return nil
|
||||
}
|
||||
|
||||
let mailActionTitle = NSLocalizedString("SHARE_ACTION_MAIL", comment: "action sheet item to open native mail app")
|
||||
return UIAlertAction(title: mailActionTitle, style: .default) { _ in
|
||||
Logger.debug("Chose mail.")
|
||||
self.channel = .mail
|
||||
|
||||
let picker = ContactsPicker(allowsMultipleSelection: true, subtitleCellType: .email)
|
||||
picker.contactsPickerDelegate = self
|
||||
picker.title = NSLocalizedString("INVITE_FRIENDS_PICKER_TITLE", comment: "Navbar title")
|
||||
let navigationController = OWSNavigationController(rootViewController: picker)
|
||||
self.presentingViewController.present(navigationController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func sendMailTo(emails recipientEmails: [String]) {
|
||||
let mailComposeViewController = MFMailComposeViewController()
|
||||
mailComposeViewController.mailComposeDelegate = self
|
||||
mailComposeViewController.setBccRecipients(recipientEmails)
|
||||
|
||||
let subject = NSLocalizedString("EMAIL_INVITE_SUBJECT", comment: "subject of email sent to contacts when inviting to install Signal")
|
||||
let bodyFormat = NSLocalizedString("EMAIL_INVITE_BODY", comment: "body of email sent to contacts when inviting to install Signal. Embeds {{link to install Signal}} and {{link to the Signal home page}}")
|
||||
let body = String.init(format: bodyFormat, installUrl, homepageUrl)
|
||||
mailComposeViewController.setSubject(subject)
|
||||
mailComposeViewController.setMessageBody(body, isHTML: false)
|
||||
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
self.presentingViewController.present(mailComposeViewController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: MailComposeViewControllerDelegate
|
||||
|
||||
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
|
||||
self.presentingViewController.dismiss(animated: true) {
|
||||
switch result {
|
||||
case .failed:
|
||||
let warning = UIAlertController(title: nil, message: NSLocalizedString("SEND_INVITE_FAILURE", comment: "Alert body after invite failed"), preferredStyle: .alert)
|
||||
warning.addAction(UIAlertAction(title: CommonStrings.dismissButton, style: .default, handler: nil))
|
||||
self.presentingViewController.present(warning, animated: true, completion: nil)
|
||||
case .sent:
|
||||
Logger.debug("user successfully invited their friends via mail.")
|
||||
case .saved:
|
||||
Logger.debug("user saved mail invite.")
|
||||
case .cancelled:
|
||||
Logger.debug("user cancelled mail invite.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -18,30 +18,6 @@ struct LegacyNotificationConfig {
|
|||
|
||||
static func notificationAction(_ action: AppNotificationAction) -> UIUserNotificationAction {
|
||||
switch action {
|
||||
// case .answerCall:
|
||||
// let mutableAction = UIMutableUserNotificationAction()
|
||||
// mutableAction.identifier = action.identifier
|
||||
// mutableAction.title = CallStrings.answerCallButtonTitle
|
||||
// mutableAction.activationMode = .foreground
|
||||
// mutableAction.isDestructive = false
|
||||
// mutableAction.isAuthenticationRequired = false
|
||||
// return mutableAction
|
||||
// case .callBack:
|
||||
// let mutableAction = UIMutableUserNotificationAction()
|
||||
// mutableAction.identifier = action.identifier
|
||||
// mutableAction.title = CallStrings.callBackButtonTitle
|
||||
// mutableAction.activationMode = .foreground
|
||||
// mutableAction.isDestructive = false
|
||||
// mutableAction.isAuthenticationRequired = true
|
||||
// return mutableAction
|
||||
// case .declineCall:
|
||||
// let mutableAction = UIMutableUserNotificationAction()
|
||||
// mutableAction.identifier = action.identifier
|
||||
// mutableAction.title = CallStrings.declineCallButtonTitle
|
||||
// mutableAction.activationMode = .background
|
||||
// mutableAction.isDestructive = false
|
||||
// mutableAction.isAuthenticationRequired = false
|
||||
// return mutableAction
|
||||
case .markAsRead:
|
||||
let mutableAction = UIMutableUserNotificationAction()
|
||||
mutableAction.identifier = action.identifier
|
||||
|
@ -176,7 +152,7 @@ extension LegacyNotificationPresenterAdaptee: NotificationPresenterAdaptee {
|
|||
}
|
||||
|
||||
let checkForCancel = category == .incomingMessage
|
||||
if checkForCancel && hasReceivedSyncMessageRecently {
|
||||
if checkForCancel {
|
||||
assert(userInfo[AppNotificationUserInfoKey.threadId] != nil)
|
||||
notification.fireDate = Date(timeIntervalSinceNow: kNotificationDelayForRemoteRead)
|
||||
notification.timeZone = NSTimeZone.local
|
||||
|
@ -283,12 +259,6 @@ public class LegacyNotificationActionHandler: NSObject {
|
|||
}
|
||||
|
||||
switch action {
|
||||
// case .answerCall:
|
||||
// return try actionHandler.answerCall(userInfo: userInfo)
|
||||
// case .callBack:
|
||||
// return try actionHandler.callBack(userInfo: userInfo)
|
||||
// case .declineCall:
|
||||
// return try actionHandler.declineCall(userInfo: userInfo)
|
||||
case .markAsRead:
|
||||
return try actionHandler.markAsRead(userInfo: userInfo)
|
||||
case .reply:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/AppContext.h>
|
||||
#import <SessionUtilitiesKit/AppContext.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
#import "MainAppContext.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <SignalCoreKit/Threading.h>
|
||||
#import <SignalUtilitiesKit/Environment.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/OWSProfileManager.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/OWSIdentityManager.h>
|
||||
#import <SessionMessagingKit/OWSIdentityManager.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue