Merge branch 'mkirk/unjank-homescreen'

This commit is contained in:
Michael Kirk 2018-04-23 12:15:50 -04:00
commit e66e5b0b97
36 changed files with 329 additions and 180 deletions

View file

@ -302,6 +302,7 @@
45360B901F9527DA00FA666C /* SearcherTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45360B8F1F9527DA00FA666C /* SearcherTest.swift */; };
45360B911F952AA900FA666C /* MarqueeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */; };
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4539B5851F79348F007141FF /* PushRegistrationManager.swift */; };
4542DF52208B82E9007B4E76 /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4542DF51208B82E9007B4E76 /* ThreadModel.swift */; };
45464DBC1DFA041F001D3FD6 /* DataChannelMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45464DBB1DFA041F001D3FD6 /* DataChannelMessage.swift */; };
454A84042059C787008B8C75 /* MediaTileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 454A84032059C787008B8C75 /* MediaTileViewController.swift */; };
454A965A1FD6017E008D2A0E /* SignalAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D913491F62D4A500722898 /* SignalAttachment.swift */; };
@ -919,6 +920,7 @@
45360B8F1F9527DA00FA666C /* SearcherTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearcherTest.swift; sourceTree = "<group>"; };
4539B5851F79348F007141FF /* PushRegistrationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushRegistrationManager.swift; sourceTree = "<group>"; };
453CC0361D08E1A60040EBA3 /* sn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sn; path = translations/sn.lproj/Localizable.strings; sourceTree = "<group>"; };
4542DF51208B82E9007B4E76 /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; };
45464DBB1DFA041F001D3FD6 /* DataChannelMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataChannelMessage.swift; sourceTree = "<group>"; };
454A84032059C787008B8C75 /* MediaTileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTileViewController.swift; sourceTree = "<group>"; };
454A965E1FD60EA2008D2A0E /* OWSFlatButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSFlatButton.swift; path = SignalMessaging/Views/OWSFlatButton.swift; sourceTree = SOURCE_ROOT; };
@ -1898,6 +1900,7 @@
45DF5DF11DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift */,
458E38351D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.h */,
458E38361D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m */,
4542DF51208B82E9007B4E76 /* ThreadModel.swift */,
);
path = Models;
sourceTree = "<group>";
@ -3199,6 +3202,7 @@
34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */,
34D99C931F2937CC00D284D6 /* OWSAnalytics.swift in Sources */,
340FC8B8204DAC8D007AEB0F /* AddToGroupViewController.m in Sources */,
4542DF52208B82E9007B4E76 /* ThreadModel.swift in Sources */,
341F2C0F1F2B8AE700D07D6B /* DebugUIMisc.m in Sources */,
340FC8AF204DAC8D007AEB0F /* OWSLinkDeviceViewController.m in Sources */,
34E3EF0D1EFC235B007F6822 /* DebugUIDiskUsage.m in Sources */,

View file

@ -0,0 +1,40 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
public class ThreadViewModel: NSObject {
let hasUnreadMessages: Bool
let lastMessageDate: Date
let isGroupThread: Bool
let threadRecord: TSThread
let unreadCount: UInt
let contactIdentifier: String?
let name: String
let isMuted: Bool
var isContactThread: Bool {
return !isGroupThread
}
let lastMessageText: String?
init(thread: TSThread, transaction: YapDatabaseReadTransaction) {
self.threadRecord = thread
self.lastMessageDate = thread.lastMessageDate()
self.isGroupThread = thread.isGroupThread()
self.name = thread.name()
self.isMuted = thread.isMuted
self.lastMessageText = thread.lastMessageText(transaction: transaction)
if let contactThread = thread as? TSContactThread {
self.contactIdentifier = contactThread.contactIdentifier()
} else {
self.contactIdentifier = nil
}
self.unreadCount = thread.unreadMessageCount(transaction: transaction)
self.hasUnreadMessages = unreadCount > 0
}
}

View file

@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN
@class TSMessage;
@class TSOutgoingMessage;
@class TSQuotedMessage;
@class YapDatabaseReadTransaction;
@protocol ConversationViewCellDelegate <NSObject>
@ -71,7 +72,7 @@ NS_ASSUME_NONNULL_BEGIN
// The width of the collection view.
@property (nonatomic) int contentWidth;
- (void)loadForDisplay;
- (void)loadForDisplayWithTransaction:(YapDatabaseReadTransaction *)transaction;
- (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth;

View file

@ -1,5 +1,5 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "ConversationViewCell.h"
@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN
self.contentWidth = 0;
}
- (void)loadForDisplay
- (void)loadForDisplayWithTransaction:(YapDatabaseReadTransaction *)transaction
{
OWSFail(@"%@ This method should be overridden.", self.logTag);
}

View file

@ -85,7 +85,7 @@ NS_ASSUME_NONNULL_BEGIN
return NSStringFromClass([self class]);
}
- (void)loadForDisplay
- (void)loadForDisplayWithTransaction:(YapDatabaseReadTransaction *)transaction
{
OWSAssert(self.viewItem);
OWSAssert([self.viewItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]);

View file

@ -150,7 +150,7 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Load
- (void)loadForDisplay
- (void)loadForDisplayWithTransaction:(YapDatabaseReadTransaction *)transaction
{
OWSAssert(self.viewItem);
OWSAssert(self.viewItem.interaction);

View file

@ -73,7 +73,7 @@ NS_ASSUME_NONNULL_BEGIN
return NSStringFromClass([self class]);
}
- (void)loadForDisplay
- (void)loadForDisplayWithTransaction:(YapDatabaseReadTransaction *)transaction
{
OWSAssert(self.viewItem);
@ -83,7 +83,7 @@ NS_ASSUME_NONNULL_BEGIN
self.imageView.image = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
self.imageView.tintColor = [self iconColorForInteraction:interaction];
self.titleLabel.textColor = [self textColor];
[self applyTitleForInteraction:interaction label:self.titleLabel];
[self applyTitleForInteraction:interaction label:self.titleLabel transaction:transaction];
[self setNeedsLayout];
}
@ -162,7 +162,9 @@ NS_ASSUME_NONNULL_BEGIN
return result;
}
- (void)applyTitleForInteraction:(TSInteraction *)interaction label:(UILabel *)label
- (void)applyTitleForInteraction:(TSInteraction *)interaction
label:(UILabel *)label
transaction:(YapDatabaseReadTransaction *)transaction
{
OWSAssert(interaction);
OWSAssert(label);
@ -170,22 +172,24 @@ NS_ASSUME_NONNULL_BEGIN
// TODO: Should we move the copy generation into this view?
if ([interaction isKindOfClass:[TSErrorMessage class]]) {
label.text = interaction.description;
TSErrorMessage *errorMessage = (TSErrorMessage *)interaction;
label.text = [errorMessage previewTextWithTransaction:transaction];
} else if ([interaction isKindOfClass:[TSInfoMessage class]]) {
if ([interaction isKindOfClass:[OWSVerificationStateChangeMessage class]]) {
OWSVerificationStateChangeMessage *message = (OWSVerificationStateChangeMessage *)interaction;
BOOL isVerified = message.verificationState == OWSVerificationStateVerified;
TSInfoMessage *infoMessage = (TSInfoMessage *)interaction;
if ([infoMessage isKindOfClass:[OWSVerificationStateChangeMessage class]]) {
OWSVerificationStateChangeMessage *verificationMessage = (OWSVerificationStateChangeMessage *)infoMessage;
BOOL isVerified = verificationMessage.verificationState == OWSVerificationStateVerified;
NSString *displayName =
[[Environment current].contactsManager displayNameForPhoneIdentifier:message.recipientId];
[[Environment current].contactsManager displayNameForPhoneIdentifier:verificationMessage.recipientId];
NSString *titleFormat = (isVerified
? (message.isLocalChange
? (verificationMessage.isLocalChange
? NSLocalizedString(@"VERIFICATION_STATE_CHANGE_FORMAT_VERIFIED_LOCAL",
@"Format for info message indicating that the verification state was verified on "
@"this device. Embeds {{user's name or phone number}}.")
: NSLocalizedString(@"VERIFICATION_STATE_CHANGE_FORMAT_VERIFIED_OTHER_DEVICE",
@"Format for info message indicating that the verification state was verified on "
@"another device. Embeds {{user's name or phone number}}."))
: (message.isLocalChange
: (verificationMessage.isLocalChange
? NSLocalizedString(@"VERIFICATION_STATE_CHANGE_FORMAT_NOT_VERIFIED_LOCAL",
@"Format for info message indicating that the verification state was unverified on "
@"this device. Embeds {{user's name or phone number}}.")
@ -194,10 +198,11 @@ NS_ASSUME_NONNULL_BEGIN
@"another device. Embeds {{user's name or phone number}}.")));
label.text = [NSString stringWithFormat:titleFormat, displayName];
} else {
label.text = interaction.description;
label.text = [infoMessage previewTextWithTransaction:transaction];
}
} else if ([interaction isKindOfClass:[TSCall class]]) {
label.text = interaction.description;
TSCall *call = (TSCall *)interaction;
label.text = [call previewTextWithTransaction:transaction];
} else {
OWSFail(@"Unknown interaction type: %@", [interaction class]);
label.text = nil;
@ -266,7 +271,11 @@ NS_ASSUME_NONNULL_BEGIN
result.height += self.topVMargin;
result.height += self.bottomVMargin;
[self applyTitleForInteraction:interaction label:self.titleLabel];
// FIXME pass in transaction from the uiDBConnection.
[[TSYapDatabaseObject dbReadConnection] readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
[self applyTitleForInteraction:interaction label:self.titleLabel transaction:transaction];
}];
CGFloat maxTitleWidth = (viewWidth - ([self hMargin] * 2.f + [self hSpacing] + [self iconSize]));
CGSize titleSize = [self.titleLabel sizeThatFits:CGSizeMake(maxTitleWidth, CGFLOAT_MAX)];

View file

@ -84,7 +84,7 @@ NS_ASSUME_NONNULL_BEGIN
return NSStringFromClass([self class]);
}
- (void)loadForDisplay
- (void)loadForDisplayWithTransaction:(YapDatabaseReadTransaction *)transaction
{
OWSAssert(self.viewItem);
OWSAssert([self.viewItem.interaction isKindOfClass:[TSUnreadIndicatorInteraction class]]);

View file

@ -4757,7 +4757,9 @@ typedef enum : NSUInteger {
}
cell.contentWidth = self.layout.contentWidth;
[cell loadForDisplay];
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
[cell loadForDisplayWithTransaction:transaction];
}];
return cell;
}

View file

@ -3223,7 +3223,7 @@ NS_ASSUME_NONNULL_BEGIN
TSContactThread *contactThread = [TSContactThread getOrCreateThreadWithContactId:phoneNumber.toE164];
[self sendFakeMessages:messageCount thread:contactThread];
DDLogError(@"Create fake thread: %@, interactions: %zd",
DDLogError(@"Create fake thread: %@, interactions: %tu",
phoneNumber.toE164,
contactThread.numberOfInteractions);
}];
@ -3251,7 +3251,7 @@ NS_ASSUME_NONNULL_BEGIN
[self sendFakeMessages:batchSize thread:thread isTextOnly:isTextOnly transaction:transaction];
}];
remainder -= batchSize;
DDLogInfo(@"%@ sendFakeMessages %zd / %zd", self.logTag, counter - remainder, counter);
DDLogInfo(@"%@ sendFakeMessages %td / %tu", self.logTag, counter - remainder, counter);
}
});
}
@ -3263,7 +3263,7 @@ NS_ASSUME_NONNULL_BEGIN
isTextOnly:(BOOL)isTextOnly
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
DDLogInfo(@"%@ sendFakeMessages: %zd", self.logTag, counter);
DDLogInfo(@"%@ sendFakeMessages: %tu", self.logTag, counter);
for (NSUInteger i = 0; i < counter; i++) {
NSString *randomText = [self randomText];
@ -3411,7 +3411,7 @@ NS_ASSUME_NONNULL_BEGIN
{
OWSAssert(thread);
DDLogInfo(@"%@ injectIncomingMessageInThread: %zd", self.logTag, counter);
DDLogInfo(@"%@ injectIncomingMessageInThread: %tu", self.logTag, counter);
NSString *randomText = [self randomText];
NSString *text = [[[@(counter) description] stringByAppendingString:@" "] stringByAppendingString:randomText];

View file

@ -5,7 +5,8 @@
NS_ASSUME_NONNULL_BEGIN
@class OWSContactsManager;
@class TSThread;
@class ThreadViewModel;
@class YapDatabaseReadTransaction;
@interface HomeViewCell : UITableViewCell
@ -13,7 +14,7 @@ NS_ASSUME_NONNULL_BEGIN
+ (NSString *)cellReuseIdentifier;
- (void)configureWithThread:(TSThread *)thread
- (void)configureWithThread:(ThreadViewModel *)thread
contactsManager:(OWSContactsManager *)contactsManager
blockedPhoneNumberSet:(NSSet<NSString *> *)blockedPhoneNumberSet;

View file

@ -12,7 +12,6 @@
#import <SignalServiceKit/OWSMessageManager.h>
#import <SignalServiceKit/TSContactThread.h>
#import <SignalServiceKit/TSGroupThread.h>
#import <SignalServiceKit/TSThread.h>
NS_ASSUME_NONNULL_BEGIN
@ -27,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic) UIView *unreadBadge;
@property (nonatomic) UILabel *unreadLabel;
@property (nonatomic, nullable) TSThread *thread;
@property (nonatomic, nullable) ThreadViewModel *thread;
@property (nonatomic, nullable) OWSContactsManager *contactsManager;
@property (nonatomic, readonly) NSMutableArray<NSLayoutConstraint *> *viewConstraints;
@ -143,9 +142,9 @@ NS_ASSUME_NONNULL_BEGIN
return NSStringFromClass(self.class);
}
- (void)configureWithThread:(TSThread *)thread
contactsManager:(OWSContactsManager *)contactsManager
blockedPhoneNumberSet:(NSSet<NSString *> *)blockedPhoneNumberSet
- (void)configureWithThread:(ThreadViewModel *)thread
contactsManager:(OWSContactsManager *)contactsManager
blockedPhoneNumberSet:(NSSet<NSString *> *)blockedPhoneNumberSet
{
OWSAssertIsOnMainThread();
OWSAssert(thread);
@ -183,7 +182,7 @@ NS_ASSUME_NONNULL_BEGIN
self.dateTimeLabel.font = self.unreadFont;
}
NSUInteger unreadCount = [[OWSMessageUtils sharedManager] unreadMessagesInThread:thread];
NSUInteger unreadCount = thread.unreadCount;
if (unreadCount == 0) {
[self.viewConstraints addObject:[self.payloadView autoPinTrailingToSuperviewMargin]];
} else {
@ -240,18 +239,19 @@ NS_ASSUME_NONNULL_BEGIN
return;
}
TSThread *thread = self.thread;
ThreadViewModel *thread = self.thread;
if (thread == nil) {
OWSFail(@"%@ thread should not be nil", self.logTag);
self.avatarView.image = nil;
return;
}
self.avatarView.image =
[OWSAvatarBuilder buildImageForThread:thread diameter:self.avatarSize contactsManager:contactsManager];
self.avatarView.image = [OWSAvatarBuilder buildImageForThread:thread.threadRecord
diameter:self.avatarSize
contactsManager:contactsManager];
}
- (NSAttributedString *)attributedSnippetForThread:(TSThread *)thread
- (NSAttributedString *)attributedSnippetForThread:(ThreadViewModel *)thread
blockedPhoneNumberSet:(NSSet<NSString *> *)blockedPhoneNumberSet
{
OWSAssert(thread);
@ -285,7 +285,7 @@ NS_ASSUME_NONNULL_BEGIN
: [UIColor lightGrayColor]),
}]];
}
NSString *displayableText = thread.lastMessageLabel.filterStringForDisplay;
NSString *displayableText = thread.lastMessageText;
if (displayableText) {
[snippetText appendAttributedString:[[NSAttributedString alloc]
initWithString:displayableText
@ -447,7 +447,7 @@ NS_ASSUME_NONNULL_BEGIN
self.nameLabel.font = self.nameFont;
TSThread *thread = self.thread;
ThreadViewModel *thread = self.thread;
if (thread == nil) {
OWSFail(@"%@ thread should not be nil", self.logTag);
self.nameLabel.attributedText = nil;

View file

@ -47,7 +47,7 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
@property (nonatomic) UISegmentedControl *segmentedControl;
@property (nonatomic) id previewingContext;
@property (nonatomic) NSSet<NSString *> *blockedPhoneNumberSet;
@property (nonatomic, readonly) NSCache<NSString *, ThreadViewModel *> *threadViewModelCache;
@property (nonatomic) BOOL isViewVisible;
@property (nonatomic) BOOL isAppInBackground;
@property (nonatomic) BOOL shouldObserveDBModifications;
@ -108,6 +108,7 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
_messageSender = [Environment current].messageSender;
_blockingManager = [OWSBlockingManager sharedManager];
_blockedPhoneNumberSet = [NSSet setWithArray:[_blockingManager blockedPhoneNumbers]];
_threadViewModelCache = [NSCache new];
// Ensure ExperienceUpgradeFinder has been initialized.
[ExperienceUpgradeFinder sharedManager];
@ -155,14 +156,14 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
_blockedPhoneNumberSet = [NSSet setWithArray:[_blockingManager blockedPhoneNumbers]];
[self.tableView reloadData];
[self reloadTableViewData];
}
- (void)signalAccountsDidChange:(id)notification
{
OWSAssertIsOnMainThread();
[self.tableView reloadData];
[self reloadTableViewData];
}
#pragma mark - View Life Cycle
@ -395,7 +396,11 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if ([TSThread numberOfKeysInCollection] > 0) {
__block BOOL hasAnyMessages;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
hasAnyMessages = [self hasAnyMessagesWithTransaction:transaction];
}];
if (hasAnyMessages) {
[self.contactsManager requestSystemContactsOnceWithCompletion:^(NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
[self updateReminderViews];
@ -468,6 +473,13 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
}
}
- (void)reloadTableViewData
{
// PERF: come up with a more nuanced cache clearing scheme
[self.threadViewModelCache removeAllObjects];
[self.tableView reloadData];
}
- (void)resetMappings
{
// If we're entering "active" mode (e.g. view is visible and app is in foreground),
@ -484,13 +496,18 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
}];
}
[[self tableView] reloadData];
[self reloadTableViewData];
[self checkIfEmptyView];
[self updateInboxCountLabel];
// If the user hasn't already granted contact access
// we don't want to request until they receive a message.
if ([TSThread numberOfKeysInCollection] > 0) {
__block BOOL hasAnyMessages;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
hasAnyMessages = [self hasAnyMessagesWithTransaction:transaction];
}];
if (hasAnyMessages) {
[self.contactsManager requestSystemContactsOnce];
}
}
@ -506,11 +523,21 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
self.isAppInBackground = YES;
}
- (BOOL)hasAnyMessagesWithTransaction:(YapDatabaseReadTransaction *)transaction
{
return [TSThread numberOfKeysInCollectionWithTransaction:transaction] > 0;
}
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
// It's possible a thread was created while we where in the background. But since we don't honor contact
// requests unless the app is in the foregrond, we must check again here upon becoming active.
if ([TSThread numberOfKeysInCollection] > 0) {
__block BOOL hasAnyMessages;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
hasAnyMessages = [self hasAnyMessagesWithTransaction:transaction];
}];
if (hasAnyMessages) {
[self.contactsManager requestSystemContactsOnceWithCompletion:^(NSError *_Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
[self updateReminderViews];
@ -577,13 +604,29 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
return (NSInteger)[self.threadMappings numberOfItemsInSection:(NSUInteger)section];
}
- (ThreadViewModel *)threadViewModelForIndexPath:(NSIndexPath *)indexPath
{
TSThread *threadRecord = [self threadForIndexPath:indexPath];
ThreadViewModel *_Nullable cachedThreadViewModel = [self.threadViewModelCache objectForKey:threadRecord.uniqueId];
if (cachedThreadViewModel) {
return cachedThreadViewModel;
}
__block ThreadViewModel *_Nullable newThreadViewModel;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
newThreadViewModel = [[ThreadViewModel alloc] initWithThread:threadRecord transaction:transaction];
}];
[self.threadViewModelCache setObject:newThreadViewModel forKey:threadRecord.uniqueId];
return newThreadViewModel;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
HomeViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:HomeViewCell.cellReuseIdentifier];
OWSAssert(cell);
TSThread *thread = [self threadForIndexPath:indexPath];
ThreadViewModel *thread = [self threadViewModelForIndexPath:indexPath];
[cell configureWithThread:thread
contactsManager:self.contactsManager
blockedPhoneNumberSet:self.blockedPhoneNumberSet];
@ -891,7 +934,7 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
[self resetMappings];
[[self tableView] reloadData];
[self reloadTableViewData];
[self checkIfEmptyView];
[self updateReminderViews];
}
@ -904,6 +947,8 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
if (!_uiDatabaseConnection) {
_uiDatabaseConnection = [OWSPrimaryStorage.sharedManager newDatabaseConnection];
// default is 250
_uiDatabaseConnection.objectCacheLimit = 500;
[_uiDatabaseConnection beginLongLivedReadTransaction];
}
return _uiDatabaseConnection;
@ -948,7 +993,12 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
// If the user hasn't already granted contact access
// we don't want to request until they receive a message.
if ([TSThread numberOfKeysInCollection] > 0) {
__block BOOL hasAnyMessages;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
hasAnyMessages = [self hasAnyMessagesWithTransaction:transaction];
}];
if (hasAnyMessages) {
[self.contactsManager requestSystemContactsOnce];
}
@ -989,6 +1039,10 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState };
}
for (YapDatabaseViewRowChange *rowChange in rowChanges) {
NSString *key = rowChange.collectionKey.key;
OWSAssert(key);
[self.threadViewModelCache removeObjectForKey:key];
switch (rowChange.type) {
case YapDatabaseViewChangeDelete: {
[self.tableView deleteRowsAtIndexPaths:@[ rowChange.indexPath ]

View file

@ -18,6 +18,7 @@
#import <SignalServiceKit/TSIncomingMessage.h>
#import <SignalServiceKit/TextSecureKitEnv.h>
#import <SignalServiceKit/Threading.h>
#import <YapDatabase/YapDatabaseTransaction.h>
@interface NotificationsManager ()
@ -200,51 +201,56 @@
#pragma mark - Signal Messages
- (void)notifyUserForErrorMessage:(TSErrorMessage *)message inThread:(TSThread *)thread {
- (void)notifyUserForErrorMessage:(TSErrorMessage *)message
thread:(TSThread *)thread
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
OWSAssert(message);
OWSAssert(thread);
dispatch_async(dispatch_get_main_queue(), ^{
if (thread.isMuted) {
return;
}
NSString *messageText = [message previewTextWithTransaction:transaction];
BOOL shouldPlaySound = [self shouldPlaySoundForNotification];
[transaction
addCompletionQueue:nil
completionBlock:^() {
if (thread.isMuted) {
return;
}
NSString *messageDescription = message.description;
BOOL shouldPlaySound = [self shouldPlaySoundForNotification];
if (([UIApplication sharedApplication].applicationState != UIApplicationStateActive) && messageDescription) {
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.userInfo = @{ Signal_Thread_UserInfo_Key : thread.uniqueId };
if (shouldPlaySound) {
OWSSound sound = [OWSSounds notificationSoundForThread:thread];
notification.soundName = [OWSSounds filenameForSound:sound];
}
if (([UIApplication sharedApplication].applicationState != UIApplicationStateActive) && messageText) {
UILocalNotification *notification = [[UILocalNotification alloc] init];
notification.userInfo = @{ Signal_Thread_UserInfo_Key : thread.uniqueId };
if (shouldPlaySound) {
OWSSound sound = [OWSSounds notificationSoundForThread:thread];
notification.soundName = [OWSSounds filenameForSound:sound];
}
NSString *alertBodyString = @"";
NSString *alertBodyString = @"";
NSString *authorName = [thread name];
switch (self.notificationPreviewType) {
case NotificationNamePreview:
case NotificationNameNoPreview:
alertBodyString = [NSString stringWithFormat:@"%@: %@", authorName, messageDescription];
break;
case NotificationNoNameNoPreview:
alertBodyString = messageDescription;
break;
}
notification.alertBody = alertBodyString;
NSString *authorName = [thread name];
switch (self.notificationPreviewType) {
case NotificationNamePreview:
case NotificationNameNoPreview:
alertBodyString = [NSString stringWithFormat:@"%@: %@", authorName, messageText];
break;
case NotificationNoNameNoPreview:
alertBodyString = messageText;
break;
}
notification.alertBody = alertBodyString;
[[PushManager sharedManager] presentNotification:notification checkForCancel:NO];
} else {
if (shouldPlaySound && [Environment.preferences soundInForeground]) {
OWSSound sound = [OWSSounds notificationSoundForThread:thread];
SystemSoundID soundId = [OWSSounds systemSoundIDForSound:sound quiet:YES];
// Vibrate, respect silent switch, respect "Alert" volume, not media volume.
AudioServicesPlayAlertSound(soundId);
}
}
});
[[PushManager sharedManager] presentNotification:notification checkForCancel:NO];
} else {
if (shouldPlaySound && [Environment.preferences soundInForeground]) {
OWSSound sound = [OWSSounds notificationSoundForThread:thread];
SystemSoundID soundId = [OWSSounds systemSoundIDForSound:sound quiet:YES];
// Vibrate, respect silent switch, respect "Alert" volume, not media volume.
AudioServicesPlayAlertSound(soundId);
}
}
}];
}
- (void)notifyUserForIncomingMessage:(TSIncomingMessage *)message

View file

@ -371,9 +371,15 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification
{
OWSAssert(recipientId.length > 0);
SignalAccount *signalAccount = [self signalAccountForRecipientId:recipientId];
SignalAccount *_Nullable signalAccount = [self signalAccountForRecipientId:recipientId];
if (!signalAccount) {
return nil;
// search system contacts for no-longer-registered signal users, for which there will be no SignalAccount
DDLogDebug(@"%@ no signal account", self.logTag);
Contact *_Nullable nonSignalContact = self.allContactsMap[recipientId];
if (!nonSignalContact) {
return nil;
}
return nonSignalContact.fullName;
}
NSString *fullName = signalAccount.contactFullName;

View file

@ -7,11 +7,11 @@ import SignalServiceKit
@objc
public class NoopNotificationsManager: NSObject, NotificationsProtocol {
public func notifyUser(for incomingMessage: TSIncomingMessage!, in thread: TSThread!, contactsManager: ContactsManagerProtocol!, transaction: YapDatabaseReadTransaction!) {
public func notifyUser(for incomingMessage: TSIncomingMessage, in thread: TSThread, contactsManager: ContactsManagerProtocol, transaction: YapDatabaseReadTransaction) {
owsFail("\(self.logTag) in \(#function).")
}
public func notifyUser(for error: TSErrorMessage!, in thread: TSThread!) {
public func notifyUser(for error: TSErrorMessage, thread: TSThread, transaction: YapDatabaseReadWriteTransaction) {
Logger.warn("\(self.logTag) in \(#function), skipping notification for: \(error.description)")
}
}

View file

@ -65,12 +65,8 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (NSArray<TSInvalidIdentityKeyReceivingErrorMessage *> *)receivedMessagesForInvalidKey:(NSData *)key;
/**
* Returns whether or not the thread has unread messages.
*
* @return YES if it has unread TSIncomingMessages, NO otherwise.
*/
- (BOOL)hasUnreadMessages;
- (NSUInteger)unreadMessageCountWithTransaction:(YapDatabaseReadTransaction *)transaction
NS_SWIFT_NAME(unreadMessageCount(transaction:));
- (BOOL)hasSafetyNumbers;
@ -90,7 +86,8 @@ NS_ASSUME_NONNULL_BEGIN
*
* @return Thread preview string.
*/
- (NSString *)lastMessageLabel;
- (NSString *)lastMessageTextWithTransaction:(YapDatabaseReadTransaction *)transaction
NS_SWIFT_NAME(lastMessageText(transaction:));
/**
* Updates the thread's caches of the latest interaction.

View file

@ -4,6 +4,7 @@
#import "TSThread.h"
#import "NSDate+OWS.h"
#import "NSString+SSK.h"
#import "OWSPrimaryStorage.h"
#import "OWSReadTracking.h"
#import "TSDatabaseView.h"
@ -24,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, copy) NSString *messageDraft;
@property (atomic, nullable) NSDate *mutedUntilDate;
- (TSInteraction *)lastInteraction;
- (TSInteraction *)lastInteractionWithTranscation:(YapDatabaseReadTransaction *)transaction;
@end
@ -199,17 +200,6 @@ NS_ASSUME_NONNULL_BEGIN
return count;
}
- (BOOL)hasUnreadMessages {
TSInteraction *interaction = self.lastInteraction;
BOOL hasUnread = NO;
if ([interaction isKindOfClass:[TSIncomingMessage class]]) {
hasUnread = ![(TSIncomingMessage *)interaction wasRead];
}
return hasUnread;
}
- (NSArray<id<OWSReadTracking>> *)unseenMessagesWithTransaction:(YapDatabaseReadTransaction *)transaction
{
NSMutableArray<id<OWSReadTracking>> *messages = [NSMutableArray new];
@ -228,6 +218,11 @@ NS_ASSUME_NONNULL_BEGIN
return [messages copy];
}
- (NSUInteger)unreadMessageCountWithTransaction:(YapDatabaseReadTransaction *)transaction
{
return [[transaction ext:TSUnreadDatabaseViewExtensionName] numberOfItemsInGroup:self.uniqueId];
}
- (void)markAllAsReadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
for (id<OWSReadTracking> message in [self unseenMessagesWithTransaction:transaction]) {
@ -238,34 +233,34 @@ NS_ASSUME_NONNULL_BEGIN
OWSAssert([self unseenMessagesWithTransaction:transaction].count < 1);
}
- (TSInteraction *) lastInteraction {
__block TSInteraction *last;
[OWSPrimaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
last = [[transaction ext:TSMessageDatabaseViewExtensionName] lastObjectInGroup:self.uniqueId];
}];
return last;
}
- (TSInteraction *)lastInteractionForInbox
- (TSInteraction *)lastInteractionForInboxWithTransaction:(YapDatabaseReadTransaction *)transaction
{
__block NSUInteger missedCount = 0;
__block TSInteraction *last = nil;
[OWSPrimaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[[transaction ext:TSMessageDatabaseViewExtensionName]
enumerateRowsInGroup:self.uniqueId
withOptions:NSEnumerationReverse
usingBlock:^(
NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) {
[[transaction ext:TSMessageDatabaseViewExtensionName]
enumerateRowsInGroup:self.uniqueId
withOptions:NSEnumerationReverse
usingBlock:^(
NSString *collection, NSString *key, id object, id metadata, NSUInteger index, BOOL *stop) {
OWSAssert([object isKindOfClass:[TSInteraction class]]);
OWSAssert([object isKindOfClass:[TSInteraction class]]);
missedCount++;
TSInteraction *interaction = (TSInteraction *)object;
if ([TSThread shouldInteractionAppearInInbox:interaction]) {
last = interaction;
TSInteraction *interaction = (TSInteraction *)object;
if ([TSThread shouldInteractionAppearInInbox:interaction]) {
last = interaction;
*stop = YES;
}
}];
}];
// For long ignored threads, with lots of SN changes this can get really slow.
// I see this in development because I have a lot of long forgotten threads with members
// who's test devices are constantly reinstalled. We could add a purpose-built DB view,
// but I think in the real world this is rare to be a hotspot.
if (missedCount > 50) {
DDLogWarn(@"%@ found last interaction for inbox after skipping %tu items", self.logTag, missedCount);
}
*stop = YES;
}
}];
return last;
}
@ -277,12 +272,14 @@ NS_ASSUME_NONNULL_BEGIN
}
}
- (NSString *)lastMessageLabel {
TSInteraction *interaction = self.lastInteractionForInbox;
if (interaction == nil) {
return @"";
- (NSString *)lastMessageTextWithTransaction:(YapDatabaseReadTransaction *)transaction
{
TSInteraction *interaction = [self lastInteractionForInboxWithTransaction:transaction];
if ([interaction conformsToProtocol:@protocol(OWSPreviewText)]) {
id<OWSPreviewText> previewable = (id<OWSPreviewText>)interaction;
return [previewable previewTextWithTransaction:transaction].filterStringForDisplay;
} else {
return interaction.description;
return @"";
}
}

View file

@ -114,6 +114,7 @@ NS_ASSUME_NONNULL_BEGIN
return !![[OWSIdentityManager sharedManager] identityKeyForRecipientId:self.contactIdentifier];
}
// TODO deprecate this? seems weird to access the displayName in the DB model
- (NSString *)name
{
return [[TextSecureKitEnv sharedEnv].contactsManager displayNameForPhoneIdentifier:self.contactIdentifier];

View file

@ -56,7 +56,7 @@ NS_ASSUME_NONNULL_BEGIN
return NO;
}
- (NSString *)description
-(NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction
{
if (self.createdByRemoteName) {
if (self.configurationIsEnabled && self.configurationDurationSeconds > 0) {

View file

@ -97,7 +97,8 @@ NSUInteger TSErrorMessageSchemaVersion = 1;
return OWSInteractionType_Error;
}
- (NSString *)description {
- (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction
{
switch (_errorType) {
case TSErrorMessageNoSession:
return NSLocalizedString(@"ERROR_MESSAGE_NO_SESSION", @"");

View file

@ -91,7 +91,8 @@ NSUInteger TSInfoMessageSchemaVersion = 1;
return OWSInteractionType_Info;
}
- (NSString *)description {
- (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction
{
switch (_messageType) {
case TSInfoMessageTypeSessionDidEnd:
return NSLocalizedString(@"SECURE_SESSION_RESET", nil);

View file

@ -19,6 +19,12 @@ typedef NS_ENUM(NSInteger, OWSInteractionType) {
OWSInteractionType_Offer,
};
@protocol OWSPreviewText
- (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction;
@end
@interface TSInteraction : TSYapDatabaseObject
- (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread;

View file

@ -127,8 +127,10 @@ NS_ASSUME_NONNULL_BEGIN
return OWSInteractionType_Unknown;
}
- (NSString *)description {
return @"Interaction description";
- (NSString *)description
{
return [NSString
stringWithFormat:@"%@ in thread: %@ timestamp: %tu", [super description], self.uniqueThreadId, self.timestamp];
}
- (void)saveWithTransaction:(YapDatabaseReadWriteTransaction *)transaction {

View file

@ -15,7 +15,7 @@ NS_ASSUME_NONNULL_BEGIN
@class TSQuotedMessage;
@class YapDatabaseReadWriteTransaction;
@interface TSMessage : TSInteraction
@interface TSMessage : TSInteraction <OWSPreviewText>
@property (nonatomic, readonly) NSMutableArray<NSString *> *attachmentIds;
@property (nonatomic, readonly, nullable) NSString *body;
@ -39,7 +39,6 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)hasAttachments;
- (nullable TSAttachment *)attachmentWithTransaction:(YapDatabaseReadTransaction *)transaction;
- (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction;
- (void)setQuotedMessageThumbnailAttachmentStream:(TSAttachmentStream *)attachmentStream;
- (BOOL)shouldStartExpireTimer;

View file

@ -265,16 +265,6 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
}
}
// TODO deprecate this and implement something like previewTextWithTransaction: for all TSInteractions
- (NSString *)description
{
__block NSString *result;
[self.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
result = [self previewTextWithTransaction:transaction];
}];
return result;
}
- (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
{
[super removeWithTransaction:transaction];

View file

@ -437,7 +437,7 @@ NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRec
[builder setBody:self.body];
} else {
OWSFail(@"%@ message body length too long.", self.logTag);
NSMutableString *truncatedBody = [self.body mutableCopy];
NSString *truncatedBody = self.body;
while ([truncatedBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kOversizeTextMessageSizeThreshold) {
DDLogError(@"%@ truncating body which is too long: %tu",
self.logTag,

View file

@ -545,7 +545,9 @@ NSString *const kNSNotificationName_IdentityStateDidChange = @"kNSNotificationNa
[message saveWithTransaction:transaction];
}
[[TextSecureKitEnv sharedEnv].notificationsManager notifyUserForErrorMessage:errorMessage inThread:contactThread];
[[TextSecureKitEnv sharedEnv].notificationsManager notifyUserForErrorMessage:errorMessage
thread:contactThread
transaction:transaction];
}
- (void)enqueueSyncMessageForVerificationStateForRecipientId:(NSString *)recipientId

View file

@ -269,8 +269,10 @@ NS_ASSUME_NONNULL_BEGIN
exception.name,
exception.reason);
__block TSErrorMessage *errorMessage;
[self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
TSErrorMessage *errorMessage;
if ([exception.name isEqualToString:NoSessionException]) {
OWSProdErrorWEnvelope([OWSAnalyticsEvents messageManagerErrorNoSession], envelope);
errorMessage = [TSErrorMessage missingSessionWithEnvelope:envelope withTransaction:transaction];
@ -298,18 +300,21 @@ NS_ASSUME_NONNULL_BEGIN
}
OWSAssert(errorMessage);
[errorMessage saveWithTransaction:transaction];
if (errorMessage != nil) {
[errorMessage saveWithTransaction:transaction];
[self notifyUserForErrorMessage:errorMessage envelope:envelope transaction:transaction];
}
}];
if (errorMessage != nil) {
[self notifyForErrorMessage:errorMessage withEnvelope:envelope];
}
}
- (void)notifyForErrorMessage:(TSErrorMessage *)errorMessage withEnvelope:(OWSSignalServiceProtosEnvelope *)envelope
- (void)notifyUserForErrorMessage:(TSErrorMessage *)errorMessage
envelope:(OWSSignalServiceProtosEnvelope *)envelope
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
TSThread *contactThread = [TSContactThread getOrCreateThreadWithContactId:envelope.source];
[[TextSecureKitEnv sharedEnv].notificationsManager notifyUserForErrorMessage:errorMessage inThread:contactThread];
TSThread *contactThread = [TSContactThread getOrCreateThreadWithContactId:envelope.source transaction:transaction];
[[TextSecureKitEnv sharedEnv].notificationsManager notifyUserForErrorMessage:errorMessage
thread:contactThread
transaction:transaction];
}
@end

View file

@ -15,7 +15,6 @@ NS_ASSUME_NONNULL_BEGIN
- (NSUInteger)unreadMessagesCount;
- (NSUInteger)unreadMessagesCountExcept:(TSThread *)thread;
- (NSUInteger)unreadMessagesInThread:(TSThread *)thread;
- (void)updateApplicationBadgeCount;

View file

@ -95,14 +95,6 @@ NS_ASSUME_NONNULL_BEGIN
[CurrentAppContext() setMainAppBadgeNumber:numberOfItems];
}
- (NSUInteger)unreadMessagesInThread:(TSThread *)thread
{
__block NSUInteger numberOfItems;
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
numberOfItems = [[transaction ext:TSUnreadDatabaseViewExtensionName] numberOfItemsInGroup:thread.uniqueId];
}];
return numberOfItems;
}
@end

View file

@ -20,7 +20,7 @@ typedef enum {
RPRecentCallTypeIncomingDeclined
} RPRecentCallType;
@interface TSCall : TSInteraction <OWSReadTracking>
@interface TSCall : TSInteraction <OWSReadTracking, OWSPreviewText>
@property (nonatomic, readonly) RPRecentCallType callType;

View file

@ -67,7 +67,9 @@ NSUInteger TSCallCurrentSchemaVersion = 1;
return OWSInteractionType_Call;
}
- (NSString *)description {
- (NSString *)previewTextWithTransaction:(YapDatabaseReadTransaction *)transaction
{
// We don't actually use the `transaction` but other sibling classes do.
switch (_callType) {
case RPRecentCallTypeIncoming:
return NSLocalizedString(@"INCOMING_CALL", @"");
@ -83,7 +85,7 @@ NSUInteger TSCallCurrentSchemaVersion = 1;
return NSLocalizedString(@"INFO_MESSAGE_MISSED_CALL_DUE_TO_CHANGED_IDENITY", @"info message text shown in conversation view");
case RPRecentCallTypeIncomingDeclined:
return NSLocalizedString(@"INCOMING_DECLINED_CALL",
@"info message recorded in conversation history when local user declined a call");
@"info message recorded in conversation history when local user declined a call");
}
}

View file

@ -1,11 +1,14 @@
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
NS_ASSUME_NONNULL_BEGIN
@class TSErrorMessage;
@class TSIncomingMessage;
@class TSThread;
@class YapDatabaseReadTransaction;
@protocol ContactsManagerProtocol;
@protocol NotificationsProtocol <NSObject>
@ -15,6 +18,10 @@
contactsManager:(id<ContactsManagerProtocol>)contactsManager
transaction:(YapDatabaseReadTransaction *)transaction;
- (void)notifyUserForErrorMessage:(TSErrorMessage *)error inThread:(TSThread *)thread;
- (void)notifyUserForErrorMessage:(TSErrorMessage *)error
thread:(TSThread *)thread
transaction:(YapDatabaseReadWriteTransaction *)transaction;
@end
NS_ASSUME_NONNULL_END

View file

@ -109,17 +109,20 @@ NS_ASSUME_NONNULL_BEGIN
+ (YapDatabaseConnection *)dbReadConnection
{
OWSJanksUI();
// We use TSYapDatabaseObject's dbReadWriteConnection (not OWSPrimaryStorage's
// dbReadConnection) for consistency, since we tend to [TSYapDatabaseObject
// save] and want to write to the same connection we read from. To get true
// consistency, we'd want to update entities by reading & writing from within
// the same transaction, but that'll be a big refactor.
return self.dbReadWriteConnection;
}
+ (YapDatabaseConnection *)dbReadWriteConnection
{
OWSJanksUI();
// Use a dedicated connection for model reads & writes.
static YapDatabaseConnection *dbReadWriteConnection = nil;
static dispatch_once_t onceToken;

View file

@ -150,4 +150,26 @@ void SwiftAssertIsOnMainThread(NSString *functionName);
userInfo:userInfoParam]; \
}
// UI JANK
//
// In pursuit of smooth UI, we want to continue moving blocking operations off the main thread.
// Add `OWSJanksUI` in code paths that shouldn't be called on the main thread.
// Because we have pervasively broken this tenant, enabling it by default would be too disruptive
// but it's helpful while unjanking and maybe someday we can have it enabled by default.
//#define DEBUG_UI_JANK 1
#ifdef DEBUG
#ifdef DEBUG_UI_JANK
#define OWSJanksUI() \
do { \
OWSAssert(![NSThread isMainThread]) \
} while (NO)
#endif
#endif
#ifndef OWSJanksUI
#define OWSJanksUI()
#endif
NS_ASSUME_NONNULL_END