Merge branch 'release/2.18.0'
This commit is contained in:
commit
0f859d6b20
|
@ -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 */,
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -67,5 +67,6 @@
|
|||
+ (void)callRecipientId:(NSString *)recipientId;
|
||||
+ (void)presentConversationForThreadId:(NSString *)threadId;
|
||||
+ (void)presentConversationForThread:(TSThread *)thread;
|
||||
+ (void)presentConversationForThread:(TSThread *)thread withCompose:(BOOL)compose;
|
||||
|
||||
@end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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 "))
|
||||
}
|
||||
}
|
|
@ -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.";
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue