Merge branch 'release/2.18.0'

This commit is contained in:
Michael Kirk 2017-10-16 12:27:20 -07:00
commit 0f859d6b20
29 changed files with 688 additions and 164 deletions

View File

@ -156,6 +156,12 @@
452EA09E1EA7ABE00078744B /* AttachmentPointerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */; };
452ECA4D1E087E7200E2F016 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; };
452ECA4E1E087E7200E2F016 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; };
4531C9C41DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 4531C9C31DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.m */; };
45360B8D1F9521F800FA666C /* Searcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45360B8C1F9521F800FA666C /* Searcher.swift */; };
45360B8E1F9521F800FA666C /* Searcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45360B8C1F9521F800FA666C /* Searcher.swift */; };
45360B901F9527DA00FA666C /* SearcherTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45360B8F1F9527DA00FA666C /* SearcherTest.swift */; };
45360B911F952AA900FA666C /* MarqueeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */; };
45360B921F952AB400FA666C /* OWSFlatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C04D7F1F6195E6004308B3 /* OWSFlatButton.swift */; };
45387B041E36D650005D00B3 /* OWS102MoveLoggingPreferenceToUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 45387B031E36D650005D00B3 /* OWS102MoveLoggingPreferenceToUserDefaults.m */; };
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4539B5851F79348F007141FF /* PushRegistrationManager.swift */; };
4539B5871F79348F007141FF /* PushRegistrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4539B5851F79348F007141FF /* PushRegistrationManager.swift */; };
@ -208,6 +214,8 @@
458E38371D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 458E38361D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m */; };
458E383A1D6699FA0094BD24 /* OWSDeviceProvisioningURLParserTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 458E38391D6699FA0094BD24 /* OWSDeviceProvisioningURLParserTest.m */; };
459311FC1D75C948008DD4F0 /* OWSDeviceTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 459311FB1D75C948008DD4F0 /* OWSDeviceTableViewCell.m */; };
45A663C51F92EC760027B59E /* GroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A663C41F92EC760027B59E /* GroupTableViewCell.swift */; };
45A663C61F92EC760027B59E /* GroupTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A663C41F92EC760027B59E /* GroupTableViewCell.swift */; };
45A6DAD61EBBF85500893231 /* ReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A6DAD51EBBF85500893231 /* ReminderView.swift */; };
45A6DAD71EBBF85500893231 /* ReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A6DAD51EBBF85500893231 /* ReminderView.swift */; };
45AE48511E0732D6004D96C2 /* TurnServerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45AE48501E0732D6004D96C2 /* TurnServerInfo.swift */; };
@ -620,6 +628,10 @@
452D1EE71DCA90D100A57EC4 /* MesssagesBubblesSizeCalculatorTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MesssagesBubblesSizeCalculatorTest.swift; path = Models/MesssagesBubblesSizeCalculatorTest.swift; sourceTree = "<group>"; };
452EA09D1EA7ABE00078744B /* AttachmentPointerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentPointerView.swift; sourceTree = "<group>"; };
452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MessageFetcherJob.swift; path = Jobs/MessageFetcherJob.swift; sourceTree = "<group>"; };
4531C9C21DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JSQMessagesCollectionViewCell+OWS.h"; sourceTree = "<group>"; };
4531C9C31DD8E6D800F08304 /* JSQMessagesCollectionViewCell+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "JSQMessagesCollectionViewCell+OWS.m"; sourceTree = "<group>"; };
45360B8C1F9521F800FA666C /* Searcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Searcher.swift; sourceTree = "<group>"; };
45360B8F1F9527DA00FA666C /* SearcherTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearcherTest.swift; sourceTree = "<group>"; };
45387B021E36D650005D00B3 /* OWS102MoveLoggingPreferenceToUserDefaults.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWS102MoveLoggingPreferenceToUserDefaults.h; path = Migrations/OWS102MoveLoggingPreferenceToUserDefaults.h; sourceTree = "<group>"; };
45387B031E36D650005D00B3 /* OWS102MoveLoggingPreferenceToUserDefaults.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWS102MoveLoggingPreferenceToUserDefaults.m; path = Migrations/OWS102MoveLoggingPreferenceToUserDefaults.m; sourceTree = "<group>"; };
4539B5851F79348F007141FF /* PushRegistrationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushRegistrationManager.swift; sourceTree = "<group>"; };
@ -677,6 +689,7 @@
459311FB1D75C948008DD4F0 /* OWSDeviceTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDeviceTableViewCell.m; sourceTree = "<group>"; };
4597E94E1D8313C100040CDE /* sq */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sq; path = translations/sq.lproj/Localizable.strings; sourceTree = "<group>"; };
4597E94F1D8313CB00040CDE /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = translations/bg.lproj/Localizable.strings; sourceTree = "<group>"; };
45A663C41F92EC760027B59E /* GroupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupTableViewCell.swift; sourceTree = "<group>"; };
45A6DAD51EBBF85500893231 /* ReminderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReminderView.swift; sourceTree = "<group>"; };
45AE48501E0732D6004D96C2 /* TurnServerInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TurnServerInfo.swift; sourceTree = "<group>"; };
45B201741DAECBFD00C461E0 /* Signal-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Signal-Bridging-Header.h"; sourceTree = "<group>"; };
@ -1487,6 +1500,7 @@
76EB04FB18170B33006006FC /* Util.h */,
45F170D51E315310003FC1F2 /* Weak.swift */,
45F170CB1E310E22003FC1F2 /* WeakTimer.swift */,
45360B8C1F9521F800FA666C /* Searcher.swift */,
);
path = util;
sourceTree = "<group>";
@ -1499,6 +1513,7 @@
34E3E5671EC4B19400495BAC /* AudioProgressView.swift */,
45F3AEB51DFDE7900080CE33 /* AvatarImageView.swift */,
451764291DE939FD00EDB8B9 /* ContactCell.swift */,
45A663C41F92EC760027B59E /* GroupTableViewCell.swift */,
451764281DE939FD00EDB8B9 /* ContactCell.xib */,
76EB052E18170B33006006FC /* ContactTableViewCell.h */,
76EB052F18170B33006006FC /* ContactTableViewCell.m */,
@ -1637,6 +1652,7 @@
B660F6B41C29868000687D6E /* UtilTest.m */,
45666F571D9B2880008FE134 /* OWSScrubbingLogFormatterTest.m */,
455AC69D1F4F8B0300134004 /* ImageCacheTest.swift */,
45360B8F1F9527DA00FA666C /* SearcherTest.swift */,
);
path = util;
sourceTree = "<group>";
@ -2314,6 +2330,7 @@
452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */,
45F170BB1E2FC5D3003FC1F2 /* CallAudioService.swift in Sources */,
34B3F8711E8DF1700035BE1A /* AboutTableViewController.m in Sources */,
45360B8D1F9521F800FA666C /* Searcher.swift in Sources */,
34B3F88D1E8DF1700035BE1A /* OWSQRCodeScanningViewController.m in Sources */,
34BECE2E1F7ABCE000D7438D /* GifPickerViewController.swift in Sources */,
34B3F8811E8DF1700035BE1A /* LockInteractionController.m in Sources */,
@ -2361,6 +2378,7 @@
76EB058818170B33006006FC /* OWSPreferences.m in Sources */,
45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */,
34D1F0B01F867BFC0066283D /* OWSSystemMessageCell.m in Sources */,
45A663C51F92EC760027B59E /* GroupTableViewCell.swift in Sources */,
34B3F87D1E8DF1700035BE1A /* FullImageViewController.m in Sources */,
45666F7B1D9C0533008FE134 /* OWSDatabaseMigration.m in Sources */,
34D1F0861F8678AA0066283D /* ConversationViewController.m in Sources */,
@ -2410,7 +2428,11 @@
4504493A1F45EE7D002D1ADA /* NSString+OWS.m in Sources */,
45843D201D2236B30013E85A /* OWSContactsSearcher.m in Sources */,
45AE48521E0732D6004D96C2 /* TurnServerInfo.swift in Sources */,
450873C41D9D5149006B54F2 /* OWSExpirationTimerView.m in Sources */,
453D28BB1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */,
45360B901F9527DA00FA666C /* SearcherTest.swift in Sources */,
B660F7561C29988E00687D6E /* PushManager.m in Sources */,
45360B911F952AA900FA666C /* MarqueeLabel.swift in Sources */,
45FBC5D21DF8592E00E9B410 /* SignalCall.swift in Sources */,
451A13B21E13DED2000A50FD /* CallNotificationsAdapter.swift in Sources */,
454EBAB31F2BC08800ACE0BB /* OWSSwiftUtils.swift in Sources */,
@ -2423,7 +2445,9 @@
B660F7721C29988E00687D6E /* AppStoreRating.m in Sources */,
B660F7751C29988E00687D6E /* UIColor+OWS.m in Sources */,
B660F7761C29988E00687D6E /* UIFont+OWS.m in Sources */,
45360B8E1F9521F800FA666C /* Searcher.swift in Sources */,
B660F7771C29988E00687D6E /* UIImage+OWS.m in Sources */,
45360B921F952AB400FA666C /* OWSFlatButton.swift in Sources */,
954AEE6A1DF33E01002E5410 /* ContactsPickerTest.swift in Sources */,
455AC69C1F4F79E500134004 /* ImageCache.swift in Sources */,
4556FA691F54AA9500AF40DD /* DebugUIProfile.swift in Sources */,
@ -2447,6 +2471,7 @@
45E7A6A81E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift in Sources */,
451DA3CB1F148AAD008E2423 /* CallViewController.swift in Sources */,
456F6E201E2411A000FD2210 /* CallService.swift in Sources */,
45A663C61F92EC760027B59E /* GroupTableViewCell.swift in Sources */,
45E615171E8C59100018AD52 /* DisplayableTextFilter.swift in Sources */,
B660F6DF1C29868000687D6E /* QueueTest.m in Sources */,
B660F6BB1C29868000687D6E /* OWSContactsManagerTest.m in Sources */,

View File

@ -833,11 +833,12 @@ static NSString *const kURLHostVerifyPrefix = @"verify";
// Fetch messages as soon as possible after launching. In particular, when
// launching from the background, without this, we end up waiting some extra
// seconds before receiving an actionable push notification.
[[Environment getCurrent].messageFetcherJob runAsync];
__unused AnyPromise *messagePromise = [[Environment getCurrent].messageFetcherJob run];
// This should happen at any launch, background or foreground.
__unused AnyPromise *promise = [OWSSyncPushTokensJob runWithAccountManager:[Environment getCurrent].accountManager
preferences:[Environment preferences]];
__unused AnyPromise *pushTokenpromise =
[OWSSyncPushTokensJob runWithAccountManager:[Environment getCurrent].accountManager
preferences:[Environment preferences]];
}
[DeviceSleepManager.sharedInstance removeBlockWithBlockObject:self];

View File

@ -8,15 +8,14 @@ import PromiseKit
@objc(OWSMessageFetcherJob)
class MessageFetcherJob: NSObject {
let TAG = "[MessageFetcherJob]"
var timer: Timer?
private let TAG = "[MessageFetcherJob]"
private var timer: Timer?
// MARK: injected dependencies
let networkManager: TSNetworkManager
let messageReceiver: OWSMessageReceiver
let signalService: OWSSignalService
var runPromises = [Double: Promise<Void>]()
private let networkManager: TSNetworkManager
private let messageReceiver: OWSMessageReceiver
private let signalService: OWSSignalService
init(messageReceiver: OWSMessageReceiver, networkManager: TSNetworkManager, signalService: OWSSignalService) {
self.messageReceiver = messageReceiver
@ -24,53 +23,58 @@ class MessageFetcherJob: NSObject {
self.signalService = signalService
}
func runAsync() {
Logger.debug("\(TAG) \(#function)")
guard signalService.isCensorshipCircumventionActive else {
public func run() -> Promise<Void> {
Logger.debug("\(TAG) in \(#function)")
guard signalService.isCensorshipCircumventionActive else {
Logger.debug("\(self.TAG) delegating message fetching to SocketManager since we're using normal transport.")
TSSocketManager.requestSocketOpen()
return
return Promise(value: ())
}
Logger.info("\(TAG) using fallback message fetching.")
Logger.info("\(TAG) fetching messages via REST.")
let promiseId = NSDate().timeIntervalSince1970
Logger.debug("\(self.TAG) starting promise: \(promiseId)")
let runPromise = self.fetchUndeliveredMessages().then { (envelopes: [OWSSignalServiceProtosEnvelope], more: Bool) -> Void in
let promise = self.fetchUndeliveredMessages().then { (envelopes: [OWSSignalServiceProtosEnvelope], more: Bool) -> Promise<Void> in
for envelope in envelopes {
Logger.info("\(self.TAG) received envelope.")
self.messageReceiver.handleReceivedEnvelope(envelope)
self.acknowledgeDelivery(envelope: envelope)
}
if more {
Logger.info("\(self.TAG) more messages, so recursing.")
// recurse
self.runAsync()
Logger.info("\(self.TAG) fetching more messages.")
return self.run()
} else {
// All finished
return Promise(value: ())
}
}.always {
Logger.debug("\(self.TAG) cleaning up promise: \(promiseId)")
self.runPromises[promiseId] = nil
}
// maintain reference to make sure it's not de-alloced prematurely.
runPromises[promiseId] = runPromise
promise.retainUntilComplete()
return promise
}
@objc func run() -> AnyPromise {
return AnyPromise(run())
}
// use in DEBUG or wherever you can't receive push notifications to poll for messages.
// Do not use in production.
func startRunLoop(timeInterval: Double) {
public func startRunLoop(timeInterval: Double) {
Logger.error("\(TAG) Starting message fetch polling. This should not be used in production.")
timer = WeakTimer.scheduledTimer(timeInterval: timeInterval, target: self, userInfo: nil, repeats: true) {[weak self] _ in
self?.runAsync()
let _: Promise<Void>? = self?.run()
return
}
}
func stopRunLoop() {
public func stopRunLoop() {
timer?.invalidate()
timer = nil
}
func parseMessagesResponse(responseObject: Any?) -> (envelopes: [OWSSignalServiceProtosEnvelope], more: Bool)? {
private func parseMessagesResponse(responseObject: Any?) -> (envelopes: [OWSSignalServiceProtosEnvelope], more: Bool)? {
guard let responseObject = responseObject else {
Logger.error("\(self.TAG) response object was surpringly nil")
return nil
@ -103,7 +107,7 @@ class MessageFetcherJob: NSObject {
)
}
func buildEnvelope(messageDict: [String: Any]) -> OWSSignalServiceProtosEnvelope? {
private func buildEnvelope(messageDict: [String: Any]) -> OWSSignalServiceProtosEnvelope? {
let builder = OWSSignalServiceProtosEnvelopeBuilder()
guard let typeInt = messageDict["type"] as? Int32 else {
@ -156,7 +160,7 @@ class MessageFetcherJob: NSObject {
return builder.build()
}
func fetchUndeliveredMessages() -> Promise<(envelopes: [OWSSignalServiceProtosEnvelope], more: Bool)> {
private func fetchUndeliveredMessages() -> Promise<(envelopes: [OWSSignalServiceProtosEnvelope], more: Bool)> {
return Promise { fulfill, reject in
let messagesRequest = OWSGetMessagesRequest()
@ -181,7 +185,7 @@ class MessageFetcherJob: NSObject {
}
}
func acknowledgeDelivery(envelope: OWSSignalServiceProtosEnvelope) {
private func acknowledgeDelivery(envelope: OWSSignalServiceProtosEnvelope) {
let request = OWSAcknowledgeMessageDeliveryRequest(source: envelope.source, timestamp: envelope.timestamp)
self.networkManager.makeRequest(request,
success: { (_: URLSessionDataTask?, _: Any?) -> Void in

View File

@ -14,6 +14,10 @@ import Foundation
}
@objc class MessageStrings: NSObject {
static let newGroupDefaultTitle = NSLocalizedString("NEW_GROUP_DEFAULT_TITLE", comment: "Used in place of the group name when a group has not yet been named.")
}
@objc class CallStrings: NSObject {
static let callStatusFormat = NSLocalizedString("CALL_STATUS_FORMAT", comment: "embeds {{Call Status}} in call screen label. For ongoing calls, {{Call Status}} is a seconds timer like 01:23, otherwise {{Call Status}} is a short text like 'Ringing', 'Busy', or 'Failed Call'")

View File

@ -289,6 +289,7 @@ open class ContactsPicker: OWSViewController, UITableViewDelegate, UITableViewDa
return nil
}
// Don't show empty sections
if dataSource[section].count > 0 {
guard section < collation.sectionTitles.count else {
return nil

View File

@ -1003,7 +1003,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) {
NSAttributedString *name;
if (self.thread.isGroupThread) {
if (self.thread.name.length == 0) {
name = [[NSAttributedString alloc] initWithString:NSLocalizedString(@"NEW_GROUP_DEFAULT_TITLE", @"")];
name = [[NSAttributedString alloc] initWithString:[MessageStrings newGroupDefaultTitle]];
} else {
name = [[NSAttributedString alloc] initWithString:self.thread.name];
}

View File

@ -216,6 +216,13 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
[emptyBoxLabel autoPinToTopLayoutGuideOfViewController:self withInset:0];
[emptyBoxLabel autoPinToBottomLayoutGuideOfViewController:self withInset:0];
UIRefreshControl *pullToRefreshView = [UIRefreshControl new];
pullToRefreshView.tintColor = [UIColor grayColor];
[pullToRefreshView addTarget:self
action:@selector(pullToRefreshPerformed:)
forControlEvents:UIControlEventValueChanged];
[self.tableView insertSubview:pullToRefreshView atIndex:0];
[self updateReminderViews];
}
@ -596,6 +603,16 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
return InboxTableViewCell.rowHeight;
}
- (void)pullToRefreshPerformed:(UIRefreshControl *)refreshControl
{
OWSAssert([NSThread isMainThread]);
DDLogInfo(@"%@ beggining refreshing.", self.tag);
[[Environment getCurrent].messageFetcherJob run].always(^{
DDLogInfo(@"%@ ending refreshing.", self.tag);
[refreshControl endRefreshing];
});
}
#pragma mark Table Swipe to Delete
- (void)tableView:(UITableView *)tableView
@ -605,7 +622,6 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
return;
}
- (NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewRowAction *deleteAction =

View File

@ -327,7 +327,7 @@ const NSUInteger kAvatarViewDiameter = 52;
NSAttributedString *name;
if (thread.isGroupThread) {
if (thread.name.length == 0) {
name = [[NSAttributedString alloc] initWithString:NSLocalizedString(@"NEW_GROUP_DEFAULT_TITLE", @"")];
name = [[NSAttributedString alloc] initWithString:[MessageStrings newGroupDefaultTitle]];
} else {
name = [[NSAttributedString alloc] initWithString:thread.name];
}

View File

@ -16,7 +16,7 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
let TAG = "[ShareActions]"
let installUrl = "https://signal.org/install/"
let homepageUrl = "https://whispersystems.org"
let homepageUrl = "https://signal.org"
let actionSheetController: UIAlertController
let presentingViewController: UIViewController
@ -98,7 +98,7 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
switch inviteChannel {
case .message:
let phoneNumbers: [String] = contacts.map { $0.userTextPhoneNumbers.first }.filter { $0 != nil }.map { $0! }
sendSMSTo(phoneNumbers: phoneNumbers)
dismissAndSendSMSTo(phoneNumbers: phoneNumbers)
case .mail:
let recipients: [String] = contacts.map { $0.emails.first }.filter { $0 != nil }.map { $0! }
sendMailTo(emails: recipients)
@ -144,26 +144,30 @@ class InviteFlow: NSObject, MFMessageComposeViewControllerDelegate, MFMailCompos
}
}
func sendSMSTo(phoneNumbers: [String]) {
public func dismissAndSendSMSTo(phoneNumbers: [String]) {
self.presentingViewController.dismiss(animated: true) {
if #available(iOS 10.0, *) {
// iOS10 message compose view doesn't respect some system appearence attributes.
// Specifically, the title is white, but the navbar is gray.
// So, we have to set system appearence before init'ing the message compose view controller in order
// to make its colors legible.
// Then we have to be sure to set it back in the ComposeViewControllerDelegate callback.
UIUtil.applyDefaultSystemAppearence()
}
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.navigationController?.present(messageComposeViewController, animated:true)
self.sendSMSTo(phoneNumbers: phoneNumbers)
}
}
public func sendSMSTo(phoneNumbers: [String]) {
if #available(iOS 10.0, *) {
// iOS10 message compose view doesn't respect some system appearence attributes.
// Specifically, the title is white, but the navbar is gray.
// So, we have to set system appearence before init'ing the message compose view controller in order
// to make its colors legible.
// Then we have to be sure to set it back in the ComposeViewControllerDelegate callback.
UIUtil.applyDefaultSystemAppearence()
}
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.navigationController?.present(messageComposeViewController, animated:true)
}
// MARK: MessageComposeViewControllerDelegate
func messageComposeViewController(_ controller: MFMessageComposeViewController, didFinishWith result: MessageComposeResult) {

View File

@ -25,6 +25,7 @@ class MessageMetadataViewController: OWSViewController {
let databaseConnection: YapDatabaseConnection
let bubbleFactory = OWSMessagesBubbleImageFactory()
var bubbleView: UIView?
var message: TSMessage
@ -69,6 +70,15 @@ class MessageMetadataViewController: OWSViewController {
createViews()
self.view.layoutIfNeeded()
if let bubbleView = self.bubbleView {
let showAtLeast: CGFloat = 50
let middleCenter = CGPoint(x: bubbleView.frame.origin.x + bubbleView.frame.width / 2,
y: bubbleView.frame.origin.y + bubbleView.frame.height - showAtLeast)
let offset = bubbleView.superview!.convert(middleCenter, to: scrollView)
self.scrollView!.setContentOffset(offset, animated: false)
}
NotificationCenter.default.addObserver(self,
selector:#selector(yapDatabaseModified),
name:NSNotification.Name.YapDatabaseModified,
@ -283,8 +293,7 @@ class MessageMetadataViewController: OWSViewController {
bodyLabel.textColor = isIncoming ? UIColor.black : UIColor.white
bodyLabel.font = UIFont.ows_regularFont(withSize:16)
bodyLabel.text = messageBody
// Only show the first N lines.
bodyLabel.numberOfLines = 10
bodyLabel.numberOfLines = 0
bodyLabel.lineBreakMode = .byWordWrapping
let bubbleImageData = isIncoming ? bubbleFactory.incoming : bubbleFactory.outgoing
@ -293,6 +302,7 @@ class MessageMetadataViewController: OWSViewController {
let trailingMargin: CGFloat = isIncoming ? 10 : 15
let bubbleView = UIImageView(image: bubbleImageData.messageBubbleImage)
self.bubbleView = bubbleView
bubbleView.layer.cornerRadius = 10
bubbleView.addSubview(bodyLabel)

View File

@ -22,6 +22,22 @@
NS_ASSUME_NONNULL_BEGIN
@interface SignalAccount (Collation)
- (NSString *)stringForCollation;
@end
@implementation SignalAccount (Collation)
- (NSString *)stringForCollation
{
OWSContactsManager *contactsManager = [Environment getCurrent].contactsManager;
return [contactsManager comparableNameForSignalAccount:self];
}
@end
@interface NewContactThreadViewController () <UISearchBarDelegate,
ContactsViewHelperDelegate,
OWSTableViewControllerDelegate,
@ -34,6 +50,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) OWSTableViewController *tableViewController;
@property (nonatomic, readonly) UILocalizedIndexedCollation *collation;
@property (nonatomic, readonly) UISearchBar *searchBar;
@property (nonatomic, readonly) NSLayoutConstraint *hideContactsPermissionReminderViewConstraint;
@ -59,6 +77,7 @@ NS_ASSUME_NONNULL_BEGIN
_contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
_nonContactAccountSet = [NSMutableSet set];
_collation = [UILocalizedIndexedCollation currentCollation];
ReminderView *contactsPermissionReminderView = [[ReminderView alloc]
initWithText:NSLocalizedString(@"COMPOSE_SCREEN_MISSING_CONTACTS_PERMISSION",
@ -285,6 +304,12 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Table Contents
- (CGFloat)actionCellHeight
{
return ScaleFromIPhone5To7Plus(round((kOWSTable_DefaultCellHeight + [ContactTableViewCell rowHeight]) * 0.5f),
[ContactTableViewCell rowHeight]);
}
- (void)updateTableContents
{
OWSTableContents *contents = [OWSTableContents new];
@ -295,38 +320,157 @@ NS_ASSUME_NONNULL_BEGIN
}
__weak NewContactThreadViewController *weakSelf = self;
ContactsViewHelper *helper = self.contactsViewHelper;
OWSTableSection *section = [OWSTableSection new];
const CGFloat kActionCellHeight
= ScaleFromIPhone5To7Plus(round((kOWSTable_DefaultCellHeight + [ContactTableViewCell rowHeight]) * 0.5f),
[ContactTableViewCell rowHeight]);
OWSTableSection *staticSection = [OWSTableSection new];
// Find Non-Contacts by Phone Number
[section addItem:[OWSTableItem
disclosureItemWithText:NSLocalizedString(@"NEW_CONVERSATION_FIND_BY_PHONE_NUMBER",
@"A label the cell that lets you add a new member to a group.")
customRowHeight:kActionCellHeight
actionBlock:^{
NewNonContactConversationViewController *viewController =
[NewNonContactConversationViewController new];
viewController.nonContactConversationDelegate = weakSelf;
[weakSelf.navigationController pushViewController:viewController animated:YES];
}]];
[staticSection
addItem:[OWSTableItem disclosureItemWithText:NSLocalizedString(@"NEW_CONVERSATION_FIND_BY_PHONE_NUMBER",
@"A label the cell that lets you add a new member to a group.")
customRowHeight:self.actionCellHeight
actionBlock:^{
NewNonContactConversationViewController *viewController =
[NewNonContactConversationViewController new];
viewController.nonContactConversationDelegate = weakSelf;
[weakSelf.navigationController pushViewController:viewController
animated:YES];
}]];
if (self.contactsViewHelper.contactsManager.isSystemContactsAuthorized) {
// Invite Contacts
[section
[staticSection
addItem:[OWSTableItem
disclosureItemWithText:NSLocalizedString(@"INVITE_FRIENDS_CONTACT_TABLE_BUTTON",
@"Label for the cell that presents the 'invite contacts' workflow.")
customRowHeight:kActionCellHeight
customRowHeight:self.actionCellHeight
actionBlock:^{
[weakSelf presentInviteFlow];
}]];
}
[contents addSection:staticSection];
BOOL hasSearchText = [self.searchBar text].length > 0;
if (hasSearchText) {
for (OWSTableSection *section in [self contactsSectionsForSearch]) {
[contents addSection:section];
}
} else {
// Count the none collated sections, before we add our collated sections.
// Later we'll need to offset which sections our collation indexes reference
// by this amount. e.g. otherwise the "B" index will reference names starting with "A"
// And the "A" index will reference the static non-collated section(s).
NSInteger noncollatedSections = (NSInteger)contents.sections.count;
for (OWSTableSection *section in [self collatedContactsSections]) {
[contents addSection:section];
}
contents.sectionForSectionIndexTitleBlock = ^NSInteger(NSString *_Nonnull title, NSInteger index) {
// Offset the collation section to account for the noncollated sections.
NSInteger sectionIndex = [self.collation sectionForSectionIndexTitleAtIndex:index] + noncollatedSections;
if (sectionIndex < 0) {
// Sentinal in case we change our section ordering in a surprising way.
OWSFail(@"Unexpected negative section index");
return 0;
}
if (sectionIndex >= (NSInteger)contents.sections.count) {
// Sentinal in case we change our section ordering in a surprising way.
OWSFail(@"Unexpectedly large index");
return 0;
}
return sectionIndex;
};
contents.sectionIndexTitlesForTableViewBlock = ^NSArray<NSString *> *_Nonnull
{
return self.collation.sectionTitles;
};
}
self.tableViewController.contents = contents;
}
- (NSArray<OWSTableSection *> *)collatedContactsSections
{
if (self.contactsViewHelper.signalAccounts.count < 1) {
// No Contacts
OWSTableSection *contactsSection = [OWSTableSection new];
if (self.contactsViewHelper.contactsManager.isSystemContactsAuthorized
&& self.contactsViewHelper.hasUpdatedContactsAtLeastOnce) {
[contactsSection
addItem:[OWSTableItem
softCenterLabelItemWithText:NSLocalizedString(@"SETTINGS_BLOCK_LIST_NO_CONTACTS",
@"A label that indicates the user has no Signal contacts.")
customRowHeight:self.actionCellHeight]];
}
return @[ contactsSection ];
}
__weak NewContactThreadViewController *weakSelf = self;
NSMutableArray<OWSTableSection *> *contactSections = [NSMutableArray new];
NSMutableArray<NSMutableArray<SignalAccount *> *> *collatedSignalAccounts = [NSMutableArray new];
for (NSUInteger i = 0; i < self.collation.sectionTitles.count; i++) {
collatedSignalAccounts[i] = [NSMutableArray new];
}
for (SignalAccount *signalAccount in self.contactsViewHelper.signalAccounts) {
NSInteger section =
[self.collation sectionForObject:signalAccount collationStringSelector:@selector(stringForCollation)];
if (section < 0) {
OWSFail(@"Unexpected collation for name:%@", signalAccount.stringForCollation);
continue;
}
NSUInteger sectionIndex = (NSUInteger)section;
[collatedSignalAccounts[sectionIndex] addObject:signalAccount];
}
for (NSUInteger i = 0; i < collatedSignalAccounts.count; i++) {
NSArray<SignalAccount *> *signalAccounts = collatedSignalAccounts[i];
NSMutableArray <OWSTableItem *> *contactItems = [NSMutableArray new];
for (SignalAccount *signalAccount in signalAccounts) {
[contactItems addObject:[OWSTableItem itemWithCustomCellBlock:^{
ContactTableViewCell *cell = [ContactTableViewCell new];
BOOL isBlocked = [self.contactsViewHelper isRecipientIdBlocked:signalAccount.recipientId];
if (isBlocked) {
cell.accessoryMessage
= NSLocalizedString(@"CONTACT_CELL_IS_BLOCKED", @"An indicator that a contact has been blocked.");
}
[cell configureWithSignalAccount:signalAccount contactsManager:self.contactsViewHelper.contactsManager];
return cell;
}
customRowHeight:[ContactTableViewCell rowHeight]
actionBlock:^{
[weakSelf newConversationWithRecipientId:signalAccount.recipientId];
}]];
}
// Don't show empty sections.
// To accomplish this we add a section with a blank title rather than omitting the section altogether,
// in order for section indexes to match up correctly
NSString *sectionTitle = contactItems.count > 0 ? self.collation.sectionTitles[i] : nil;
[contactSections addObject:[OWSTableSection sectionWithTitle:sectionTitle items:contactItems]];
}
return [contactSections copy];
}
- (NSArray<OWSTableSection *> *)contactsSectionsForSearch
{
__weak NewContactThreadViewController *weakSelf = self;
NSMutableArray<OWSTableSection *> *sections = [NSMutableArray new];
ContactsViewHelper *helper = self.contactsViewHelper;
OWSTableSection *phoneNumbersSection = [OWSTableSection new];
// FIXME we should make sure "invite via SMS" cells appear *below* any matching signal-account cells.
//
// If the search string looks like a phone number, show either "new conversation..." cells and/or
// "invite via SMS..." cells.
NSArray<NSString *> *searchPhoneNumbers = [self parsePossibleSearchPhoneNumbers];
@ -334,7 +478,7 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert(phoneNumber.length > 0);
if ([self.nonContactAccountSet containsObject:phoneNumber]) {
[section addItem:[OWSTableItem itemWithCustomCellBlock:^{
[phoneNumbersSection addItem:[OWSTableItem itemWithCustomCellBlock:^{
ContactTableViewCell *cell = [ContactTableViewCell new];
BOOL isBlocked = [helper isRecipientIdBlocked:phoneNumber];
if (isBlocked) {
@ -351,32 +495,42 @@ NS_ASSUME_NONNULL_BEGIN
return cell;
}
customRowHeight:[ContactTableViewCell rowHeight]
actionBlock:^{
[weakSelf newConversationWith:phoneNumber];
}]];
customRowHeight:[ContactTableViewCell rowHeight]
actionBlock:^{
[weakSelf newConversationWithRecipientId:phoneNumber];
}]];
} else {
NSString *text = [NSString stringWithFormat:NSLocalizedString(@"SEND_INVITE_VIA_SMS_BUTTON_FORMAT",
@"Text for button to send a Signal invite via SMS. %@ is "
@"placeholder for the receipient's phone number."),
phoneNumber];
[section addItem:[OWSTableItem disclosureItemWithText:text
customRowHeight:kActionCellHeight
actionBlock:^{
[weakSelf sendTextToPhoneNumber:phoneNumber];
}]];
[phoneNumbersSection addItem:[OWSTableItem disclosureItemWithText:text
customRowHeight:self.actionCellHeight
actionBlock:^{
[weakSelf sendTextToPhoneNumber:phoneNumber];
}]];
}
}
if (searchPhoneNumbers.count > 0) {
[sections addObject:phoneNumbersSection];
}
// Contacts, possibly filtered with the search text.
// Contacts, filtered with the search text.
NSArray<SignalAccount *> *filteredSignalAccounts = [self filteredSignalAccounts];
BOOL hasSearchResults = NO;
OWSTableSection *contactsSection = [OWSTableSection new];
contactsSection.headerTitle = NSLocalizedString(@"COMPOSE_MESSAGE_CONTACT_SECTION_TITLE",
@"Table section header for contact listing when composing a new message");
for (SignalAccount *signalAccount in filteredSignalAccounts) {
hasSearchResults = YES;
if ([searchPhoneNumbers containsObject:signalAccount.recipientId]) {
// Don't show a contact if they already appear in the "search phone numbers"
// results.
continue;
}
[section addItem:[OWSTableItem itemWithCustomCellBlock:^{
[contactsSection addItem:[OWSTableItem itemWithCustomCellBlock:^{
ContactTableViewCell *cell = [ContactTableViewCell new];
BOOL isBlocked = [helper isRecipientIdBlocked:signalAccount.recipientId];
if (isBlocked) {
@ -388,76 +542,124 @@ NS_ASSUME_NONNULL_BEGIN
return cell;
}
customRowHeight:[ContactTableViewCell rowHeight]
actionBlock:^{
[weakSelf newConversationWith:signalAccount.recipientId];
}]];
customRowHeight:[ContactTableViewCell rowHeight]
actionBlock:^{
[weakSelf newConversationWithRecipientId:signalAccount.recipientId];
}]];
}
if (filteredSignalAccounts.count > 0) {
[sections addObject:contactsSection];
}
BOOL hasSearchText = [self.searchBar text].length > 0;
BOOL hasSearchResults = filteredSignalAccounts.count > 0;
// When searching, we include matching groups
OWSTableSection *groupSection = [OWSTableSection new];
groupSection.headerTitle = NSLocalizedString(
@"COMPOSE_MESSAGE_GROUP_SECTION_TITLE", @"Table section header for group listing when composing a new message");
NSArray<TSGroupThread *> *filteredGroupThreads = [self filteredGroupThreads];
for (TSGroupThread *thread in filteredGroupThreads) {
hasSearchResults = YES;
[groupSection addItem:[OWSTableItem itemWithCustomCellBlock:^{
GroupTableViewCell *cell = [GroupTableViewCell new];
[cell configureWithThread:thread contactsManager:helper.contactsManager];
return cell;
}
customRowHeight:[ContactTableViewCell rowHeight]
actionBlock:^{
[weakSelf newConversationWithThread:thread];
}]];
}
if (filteredGroupThreads.count > 0) {
[sections addObject:groupSection];
}
// Invitation offers for non-signal contacts
if (hasSearchText) {
for (Contact *contact in [helper nonSignalContactsMatchingSearchString:[self.searchBar text]]) {
hasSearchResults = YES;
OWSTableSection *inviteeSection = [OWSTableSection new];
inviteeSection.headerTitle = NSLocalizedString(@"COMPOSE_MESSAGE_INVITE_SECTION_TITLE",
@"Table section header for invite listing when composing a new message");
NSArray<Contact *> *invitees = [helper nonSignalContactsMatchingSearchString:[self.searchBar text]];
for (Contact *contact in invitees) {
hasSearchResults = YES;
OWSAssert(contact.parsedPhoneNumbers.count > 0);
// TODO: Should we invite all of their phone numbers?
PhoneNumber *phoneNumber = contact.parsedPhoneNumbers[0];
NSString *displayName = contact.fullName;
if (displayName.length < 1) {
displayName = phoneNumber.toE164;
}
NSString *text = [NSString stringWithFormat:NSLocalizedString(@"SEND_INVITE_VIA_SMS_BUTTON_FORMAT",
@"Text for button to send a Signal invite via SMS. %@ is "
@"placeholder for the receipient's phone number."),
displayName];
[section addItem:[OWSTableItem disclosureItemWithText:text
customRowHeight:kActionCellHeight
actionBlock:^{
[weakSelf sendTextToPhoneNumber:phoneNumber.toE164];
}]];
OWSAssert(contact.parsedPhoneNumbers.count > 0);
// TODO: Should we invite all of their phone numbers?
PhoneNumber *phoneNumber = contact.parsedPhoneNumbers[0];
NSString *displayName = contact.fullName;
if (displayName.length < 1) {
displayName = phoneNumber.toE164;
}
NSString *text = [NSString stringWithFormat:NSLocalizedString(@"SEND_INVITE_VIA_SMS_BUTTON_FORMAT",
@"Text for button to send a Signal invite via SMS. %@ is "
@"placeholder for the receipient's phone number."),
displayName];
[inviteeSection addItem:[OWSTableItem disclosureItemWithText:text
customRowHeight:self.actionCellHeight
actionBlock:^{
[weakSelf sendTextToPhoneNumber:phoneNumber.toE164];
}]];
}
if (invitees.count > 0) {
[sections addObject:inviteeSection];
}
if (!hasSearchText && helper.signalAccounts.count < 1) {
// No Contacts
if (self.contactsViewHelper.contactsManager.isSystemContactsAuthorized
&& self.contactsViewHelper.hasUpdatedContactsAtLeastOnce) {
[section
addItem:[OWSTableItem
softCenterLabelItemWithText:NSLocalizedString(@"SETTINGS_BLOCK_LIST_NO_CONTACTS",
@"A label that indicates the user has no Signal contacts.")
customRowHeight:kActionCellHeight]];
}
}
if (hasSearchText && !hasSearchResults) {
if (!hasSearchResults) {
// No Search Results
OWSTableSection *noResultsSection = [OWSTableSection new];
[noResultsSection
addItem:[OWSTableItem softCenterLabelItemWithText:
NSLocalizedString(@"SETTINGS_BLOCK_LIST_NO_SEARCH_RESULTS",
@"A label that indicates the user's search has no matching results.")
customRowHeight:self.actionCellHeight]];
[section addItem:[OWSTableItem softCenterLabelItemWithText:
NSLocalizedString(@"SETTINGS_BLOCK_LIST_NO_SEARCH_RESULTS",
@"A label that indicates the user's search has no matching results.")
customRowHeight:kActionCellHeight]];
[sections addObject:noResultsSection];
}
[contents addSection:section];
self.tableViewController.contents = contents;
return [sections copy];
}
- (NSArray<SignalAccount *> *)filteredSignalAccounts
{
NSString *searchString = [self.searchBar text];
NSString *searchString = self.searchBar.text;
ContactsViewHelper *helper = self.contactsViewHelper;
return [helper signalAccountsMatchingSearchString:searchString];
}
- (NSArray<TSGroupThread *> *)filteredGroupThreads
{
AnySearcher *searcher = [[AnySearcher alloc] initWithIndexer:^NSString * _Nonnull(id _Nonnull obj) {
if (![obj isKindOfClass:[TSGroupThread class]]) {
OWSFail(@"unexpected item in searcher");
return @"";
}
TSGroupThread *groupThread = (TSGroupThread *)obj;
NSString *groupName = groupThread.groupModel.groupName;
NSMutableString *groupMemberNames = [NSMutableString new];
for (NSString *recipientId in groupThread.groupModel.groupMemberIds) {
NSString *contactName = [self.contactsViewHelper.contactsManager displayNameForPhoneIdentifier:recipientId];
[groupMemberNames appendFormat:@" %@", contactName];
}
return [NSString stringWithFormat:@"%@ %@", groupName, groupMemberNames];
}];
NSMutableArray<TSGroupThread *> *matchingThreads = [NSMutableArray new];
[TSGroupThread enumerateCollectionObjectsUsingBlock:^(id obj, BOOL *stop) {
if (![obj isKindOfClass:[TSGroupThread class]]) {
// group and contact threads are in the same collection.
return;
}
TSGroupThread *groupThread = (TSGroupThread *)obj;
if ([searcher item:groupThread doesMatchQuery:self.searchBar.text]) {
[matchingThreads addObject:groupThread];
}
}];
return [matchingThreads copy];
}
#pragma mark - No Contacts Mode
- (void)hideBackgroundView
@ -520,6 +722,11 @@ NS_ASSUME_NONNULL_BEGIN
- (void)sendTextToPhoneNumber:(NSString *)phoneNumber
{
OWSInviteFlow *inviteFlow =
[[OWSInviteFlow alloc] initWithPresentingViewController:self
contactsManager:self.contactsViewHelper.contactsManager];
OWSAssert([phoneNumber length] > 0);
NSString *confirmMessage = NSLocalizedString(@"SEND_SMS_CONFIRM_TITLE", @"");
if ([phoneNumber length] > 0) {
@ -537,18 +744,8 @@ NS_ASSUME_NONNULL_BEGIN
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
[self.searchBar resignFirstResponder];
if ([MFMessageComposeViewController canSendText]) {
MFMessageComposeViewController *picker = [[MFMessageComposeViewController alloc] init];
picker.messageComposeDelegate = self;
picker.recipients = @[
phoneNumber,
];
picker.body = [NSLocalizedString(@"SMS_INVITE_BODY", @"")
stringByAppendingString:
@" https://itunes.apple.com/us/app/signal-private-messenger/id874139669?mt=8"];
[self presentViewController:picker animated:YES completion:[UIUtil modalCompletionBlock]];
[inviteFlow sendSMSToPhoneNumbers:@[ phoneNumber ]];
} else {
[OWSAlerts showAlertWithTitle:NSLocalizedString(@"ALERT_ERROR_TITLE", @"")
message:NSLocalizedString(@"UNSUPPORTED_FEATURE_ERROR", @"")];
@ -558,6 +755,7 @@ NS_ASSUME_NONNULL_BEGIN
[alertController addAction:[OWSAlerts cancelAction]];
[alertController addAction:okAction];
self.searchBar.text = @"";
[self searchTextDidChange];
// must dismiss search controller before presenting alert.
if ([self presentedViewController]) {
@ -619,13 +817,19 @@ NS_ASSUME_NONNULL_BEGIN
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)newConversationWith:(NSString *)recipientId
- (void)newConversationWithRecipientId:(NSString *)recipientId
{
OWSAssert(recipientId.length > 0);
TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:recipientId];
[self newConversationWithThread:thread];
}
- (void)newConversationWithThread:(TSThread *)thread
{
OWSAssert(thread != nil);
[self dismissViewControllerAnimated:YES
completion:^() {
[Environment presentConversationForRecipientId:recipientId withCompose:YES];
[Environment presentConversationForThread:thread withCompose:YES];
}];
}
@ -662,7 +866,7 @@ NS_ASSUME_NONNULL_BEGIN
{
OWSAssert(recipientId.length > 0);
[self newConversationWith:recipientId];
[self newConversationWithRecipientId:recipientId];
}
#pragma mark - UISearchBarDelegate

View File

@ -98,7 +98,7 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68;
{
[super loadView];
self.title = NSLocalizedString(@"NEW_GROUP_DEFAULT_TITLE", @"The navbar title for the 'new group' view.");
self.title = [MessageStrings newGroupDefaultTitle];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
initWithTitle:NSLocalizedString(@"NEW_GROUP_CREATE_BUTTON", @"The title for the 'create group' button.")

View File

@ -126,7 +126,7 @@ NS_ASSUME_NONNULL_BEGIN
threadName =
[PhoneNumber bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:self.thread.contactIdentifier];
} else if (threadName.length == 0 && [self isGroupThread]) {
threadName = NSLocalizedString(@"NEW_GROUP_DEFAULT_TITLE", @"");
threadName = [MessageStrings newGroupDefaultTitle];
}
return threadName;
}

View File

@ -14,7 +14,10 @@ extern const CGFloat kOWSTable_DefaultCellHeight;
@interface OWSTableContents : NSObject
@property (nonatomic) NSString *title;
@property (nonatomic, nullable) NSInteger (^sectionForSectionIndexTitleBlock)(NSString *title, NSInteger index);
@property (nonatomic, nullable) NSArray<NSString *> * (^sectionIndexTitlesForTableViewBlock)(void);
@property (nonatomic, readonly) NSArray<OWSTableSection *> *sections;
- (void)addSection:(OWSTableSection *)section;
@end

View File

@ -509,19 +509,36 @@ NSString * const kOWSTableCellIdentifier = @"kOWSTableCellIdentifier";
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)sectionIndex
{
OWSTableSection *section = [self sectionForIndex:sectionIndex];
if (section && section.customHeaderHeight) {
return [section.customHeaderHeight floatValue];
if (!section) {
OWSFail(@"Section index out of bounds.");
return 0;
}
if (section.customHeaderHeight) {
return [section.customHeaderHeight floatValue];
} else if (section.headerTitle.length > 0) {
return UITableViewAutomaticDimension;
} else {
return 0;
}
return UITableViewAutomaticDimension;
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)sectionIndex
{
OWSTableSection *section = [self sectionForIndex:sectionIndex];
if (section && section.customFooterHeight) {
return [section.customFooterHeight floatValue];
if (!section) {
OWSFail(@"Section index out of bounds.");
return 0;
}
if (section.customFooterHeight) {
return [section.customFooterHeight floatValue];
} else if (section.footerTitle.length > 0) {
return UITableViewAutomaticDimension;
} else {
return 0;
}
return UITableViewAutomaticDimension;
}
// Called before the user changes the selection. Return a new indexPath, or nil, to change the proposed selection.
@ -545,6 +562,26 @@ NSString * const kOWSTableCellIdentifier = @"kOWSTableCellIdentifier";
}
}
#pragma mark Index
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
{
if (self.contents.sectionForSectionIndexTitleBlock) {
return self.contents.sectionForSectionIndexTitleBlock(title, index);
} else {
return 0;
}
}
- (nullable NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
if (self.contents.sectionIndexTitlesForTableViewBlock) {
return self.contents.sectionIndexTitlesForTableViewBlock();
} else {
return 0;
}
}
#pragma mark - Logging
+ (NSString *)tag

View File

@ -80,6 +80,11 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification;
- (NSString *)displayNameForPhoneIdentifier:(nullable NSString *)identifier;
- (NSString *)displayNameForSignalAccount:(SignalAccount *)signalAccount;
/**
* Used for sorting, respects system contacts name sort order preference.
*/
- (NSString *)comparableNameForSignalAccount:(SignalAccount *)signalAccount;
// Generally we prefer the formattedProfileName over the raw profileName so as to
// distinguish a profile name apart from a name pulled from the system's contacts.
// This helps clarify when the remote person chooses a potentially confusing profile name.

View File

@ -709,6 +709,24 @@ NSString *const kTSStorageManager_lastKnownContactRecipientIds = @"lastKnownCont
return image;
}
- (NSString *)comparableNameForSignalAccount:(SignalAccount *)signalAccount
{
NSString *_Nullable name;
if (signalAccount.contact) {
if (ABPersonGetSortOrdering() == kABPersonSortByFirstName) {
name = signalAccount.contact.comparableNameFirstLast;
} else {
name = signalAccount.contact.comparableNameLastFirst;
}
}
if (name.length < 1) {
name = signalAccount.recipientId;
}
return name;
}
#pragma mark - Logging
+ (NSString *)tag

View File

@ -67,5 +67,6 @@
+ (void)callRecipientId:(NSString *)recipientId;
+ (void)presentConversationForThreadId:(NSString *)threadId;
+ (void)presentConversationForThread:(TSThread *)thread;
+ (void)presentConversationForThread:(TSThread *)thread withCompose:(BOOL)compose;
@end

View File

@ -247,7 +247,12 @@ static Environment *environment = nil;
+ (void)presentConversationForThread:(TSThread *)thread
{
[self presentConversationForThread:thread keyboardOnViewAppearing:YES callOnViewAppearing:NO];
[self presentConversationForThread:thread withCompose:YES];
}
+ (void)presentConversationForThread:(TSThread *)thread withCompose:(BOOL)compose
{
[self presentConversationForThread:thread keyboardOnViewAppearing:compose callOnViewAppearing:NO];
}
+ (void)presentConversationForThread:(TSThread *)thread

View File

@ -274,7 +274,7 @@ NSString *const kNotificationsManagerNewMesssageSoundName = @"NewMessage.aifc";
NSString *senderName = [contactsManager displayNameForPhoneIdentifier:message.authorId];
NSString *groupName = [thread.name stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if (groupName.length < 1) {
groupName = NSLocalizedString(@"NEW_GROUP_DEFAULT_TITLE", @"");
groupName = [MessageStrings newGroupDefaultTitle];
}
if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive && messageDescription) {

View File

@ -101,11 +101,11 @@ NSString *const Signal_Message_MarkAsRead_Identifier = @"Signal_Message_MarkAsRe
{
DDLogInfo(@"%@ received remote notification", self.tag);
[self.messageFetcherJob runAsync];
[self.messageFetcherJob run];
}
- (void)applicationDidBecomeActive {
[self.messageFetcherJob runAsync];
[self.messageFetcherJob run];
}
/**

View File

@ -0,0 +1,45 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import Foundation
// ObjC compatible searcher
@objc class AnySearcher: NSObject {
private let searcher: Searcher<AnyObject>
public init(indexer: @escaping (AnyObject) -> String ) {
searcher = Searcher(indexer: indexer)
super.init()
}
@objc(item:doesMatchQuery:)
public func matches(item: AnyObject, query: String) -> Bool {
return searcher.matches(item: item, query: query)
}
}
class Searcher<T> {
private let indexer: (T) -> String
public init(indexer: @escaping (T) -> String) {
self.indexer = indexer
}
public func matches(item: T, query: String) -> Bool {
let itemString = normalize(string: indexer(item))
return stem(string: query).map { queryStem in
return itemString.contains(queryStem)
}.reduce(true) { $0 && $1 }
}
private func stem(string: String) -> [String] {
return normalize(string: string).components(separatedBy: .whitespaces)
}
private func normalize(string: String) -> String {
return string.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
}
}

View File

@ -14,6 +14,8 @@
NS_ASSUME_NONNULL_BEGIN
extern NSString *const kContactsTable_CellReuseIdentifier;
extern const NSUInteger kContactTableViewCellAvatarSize;
extern const CGFloat kContactTableViewCellAvatarTextMargin;
@class OWSContactsManager;
@class SignalAccount;

View File

@ -18,6 +18,7 @@ NS_ASSUME_NONNULL_BEGIN
NSString *const kContactsTable_CellReuseIdentifier = @"kContactsTable_CellReuseIdentifier";
const NSUInteger kContactTableViewCellAvatarSize = 40;
const CGFloat kContactTableViewCellAvatarTextMargin = 12;
@interface ContactTableViewCell ()
@ -107,7 +108,7 @@ const NSUInteger kContactTableViewCellAvatarSize = 40;
[_subtitle autoPinEdgeToSuperviewEdge:ALEdgeBottom];
[_nameContainerView autoVCenterInSuperview];
[_nameContainerView autoPinLeadingToTrailingOfView:_avatarView margin:12.f];
[_nameContainerView autoPinLeadingToTrailingOfView:_avatarView margin:kContactTableViewCellAvatarTextMargin];
[_nameContainerView autoPinTrailingToSuperview];
// Force layout, since imageView isn't being initally rendered on App Store optimized build.
@ -158,7 +159,7 @@ const NSUInteger kContactTableViewCellAvatarSize = 40;
NSString *threadName = thread.name;
if (threadName.length == 0 && [thread isKindOfClass:[TSGroupThread class]]) {
threadName = NSLocalizedString(@"NEW_GROUP_DEFAULT_TITLE", @"");
threadName = [MessageStrings newGroupDefaultTitle];
}
NSAttributedString *attributedText = [[NSAttributedString alloc]

View File

@ -0,0 +1,69 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import UIKit
@objc class GroupTableViewCell: UITableViewCell {
let TAG = "[GroupTableViewCell]"
private let avatarView = AvatarImageView()
private let nameLabel = UILabel()
private let subtitleLabel = UILabel()
init() {
super.init(style: .default, reuseIdentifier: TAG)
self.contentView.addSubview(avatarView)
let textContainer = UIView.container()
textContainer.addSubview(nameLabel)
textContainer.addSubview(subtitleLabel)
self.contentView.addSubview(textContainer)
// Font config
nameLabel.font = UIFont.ows_dynamicTypeBody()
subtitleLabel.font = UIFont.ows_footnote()
subtitleLabel.textColor = UIColor.ows_darkGray()
// Listen to notifications...
// TODO avatar, group name change, group membership change, group member name change
// Layout
nameLabel.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets.zero, excludingEdge: .bottom)
subtitleLabel.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets.zero, excludingEdge: .top)
subtitleLabel.autoPinEdge(.top, to: .bottom, of: nameLabel)
avatarView.autoPinLeadingToSuperview()
avatarView.autoVCenterInSuperview()
avatarView.autoSetDimension(.width, toSize: CGFloat(kContactTableViewCellAvatarSize))
avatarView.autoPinToSquareAspectRatio()
textContainer.autoPinEdge(.leading, to: .trailing, of: avatarView, withOffset: kContactTableViewCellAvatarTextMargin)
textContainer.autoPinEdge(toSuperviewEdge: .trailing)
textContainer.autoVCenterInSuperview()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public func configure(thread: TSGroupThread, contactsManager: OWSContactsManager) {
if let groupName = thread.groupModel.groupName, !groupName.isEmpty {
self.nameLabel.text = groupName
} else {
self.nameLabel.text = MessageStrings.newGroupDefaultTitle
}
let groupMemberIds: [String] = thread.groupModel.groupMemberIds
let groupMemberNames = groupMemberIds.map { (recipientId: String) in
contactsManager.displayName(forPhoneIdentifier: recipientId)
}.joined(separator: ", ")
self.subtitleLabel.text = groupMemberNames
self.avatarView.image = OWSAvatarBuilder.buildImage(thread: thread, diameter: kContactTableViewCellAvatarSize, contactsManager: contactsManager)
}
}

View File

@ -0,0 +1,60 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import XCTest
class SearcherTest: XCTestCase {
struct TestCharacter {
let name: String
let description: String
}
let smerdyakov = TestCharacter(name: "Pavel Fyodorovich Smerdyakov", description: "A rusty hue in the sky")
let stinkingLizaveta = TestCharacter(name: "Stinking Lizaveta", description: "object of pity")
let regularLizaveta = TestCharacter(name: "Lizaveta", description: "")
let indexer = { (character: TestCharacter) in
return "\(character.name) \(character.description)"
}
var searcher: Searcher<TestCharacter> {
return Searcher(indexer: indexer)
}
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testSimple() {
XCTAssert(searcher.matches(item: smerdyakov, query: "Pavel"))
XCTAssert(searcher.matches(item: smerdyakov, query: "pavel"))
XCTAssertFalse(searcher.matches(item: smerdyakov, query: "asdf"))
XCTAssertFalse(searcher.matches(item: smerdyakov, query: ""))
XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Pity"))
}
func testRepeats() {
XCTAssert(searcher.matches(item: smerdyakov, query: "pavel pavel"))
XCTAssertFalse(searcher.matches(item: smerdyakov, query: "pavelpavel"))
}
func testSplitWords() {
XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Lizaveta"))
XCTAssert(searcher.matches(item: regularLizaveta, query: "Lizaveta"))
XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Stinking Lizaveta"))
XCTAssertFalse(searcher.matches(item: regularLizaveta, query: "Stinking Lizaveta"))
XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Lizaveta Stinking"))
XCTAssert(searcher.matches(item: stinkingLizaveta, query: "Lizaveta St"))
XCTAssert(searcher.matches(item: stinkingLizaveta, query: " Lizaveta St "))
}
}

View File

@ -256,6 +256,15 @@
/* Activity Sheet label */
"COMPARE_SAFETY_NUMBER_ACTION" = "Compare with Clipboard";
/* Table section header for contact listing when composing a new message */
"COMPOSE_MESSAGE_CONTACT_SECTION_TITLE" = "Contacts";
/* Table section header for group listing when composing a new message */
"COMPOSE_MESSAGE_GROUP_SECTION_TITLE" = "Groups";
/* Table section header for invite listing when composing a new message */
"COMPOSE_MESSAGE_INVITE_SECTION_TITLE" = "Invite";
/* Multiline label explaining why compose-screen contact picker is empty. */
"COMPOSE_SCREEN_MISSING_CONTACTS_PERMISSION" = "To see which of your contacts are Signal users, allow contacts access in your system settings.";

View File

@ -7,7 +7,7 @@
@interface TSGroupModel : TSYapDatabaseObject
@property (nonatomic, strong) NSMutableArray<NSString *> *groupMemberIds;
@property (nonatomic, strong) NSArray<NSString *> *groupMemberIds;
@property (nonatomic, strong) NSString *groupName;
@property (nonatomic, strong) NSData *groupId;

View File

@ -9,7 +9,7 @@
#if TARGET_OS_IOS
- (instancetype)initWithTitle:(NSString *)title
memberIds:(NSMutableArray<NSString *> *)memberIds
memberIds:(NSArray<NSString *> *)memberIds
image:(UIImage *)image
groupId:(NSData *)groupId
{