session-ios/Signal/src/ViewControllers/HomeView/HomeViewController.m

1794 lines
70 KiB
Mathematica
Raw Normal View History

2014-10-29 21:58:58 +01:00
//
2019-01-08 16:44:22 +01:00
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
2014-10-29 21:58:58 +01:00
//
2017-09-06 19:59:39 +02:00
#import "HomeViewController.h"
#import "AppDelegate.h"
#import "AppSettingsViewController.h"
2018-04-10 19:02:33 +02:00
#import "HomeViewCell.h"
#import "NewContactThreadViewController.h"
#import "OWSNavigationController.h"
#import "OWSPrimaryStorage.h"
#import "ProfileViewController.h"
2018-06-18 17:28:21 +02:00
#import "RegistrationUtils.h"
2019-05-02 23:58:48 +02:00
#import "Session-Swift.h"
#import "SignalApp.h"
#import "TSAccountManager.h"
#import "TSDatabaseView.h"
#import "TSGroupThread.h"
#import "ViewControllerUtils.h"
#import <PromiseKit/AnyPromise.h>
2020-06-05 02:38:44 +02:00
#import <SessionCoreKit/NSDate+OWS.h>
#import <SessionCoreKit/Threading.h>
#import <SessionCoreKit/iOSVersions.h>
2017-12-19 03:50:51 +01:00
#import <SignalMessaging/OWSContactsManager.h>
#import <SignalMessaging/OWSFormat.h>
#import <SignalMessaging/SignalMessaging-Swift.h>
2019-02-18 17:44:27 +01:00
#import <SignalMessaging/Theme.h>
2017-12-08 17:50:35 +01:00
#import <SignalMessaging/UIUtil.h>
2020-06-05 02:38:44 +02:00
#import <SessionServiceKit/OWSMessageSender.h>
#import <SessionServiceKit/OWSMessageUtils.h>
#import <SessionServiceKit/TSAccountManager.h>
#import <SessionServiceKit/TSOutgoingMessage.h>
2018-08-07 23:36:34 +02:00
#import <StoreKit/StoreKit.h>
2017-12-20 17:28:07 +01:00
#import <YapDatabase/YapDatabase.h>
#import <YapDatabase/YapDatabaseViewChange.h>
#import <YapDatabase/YapDatabaseViewConnection.h>
2018-06-12 17:27:32 +02:00
NS_ASSUME_NONNULL_BEGIN
NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversationsReuseIdentifier";
typedef NS_ENUM(NSInteger, HomeViewMode) {
HomeViewMode_Archive,
HomeViewMode_Inbox,
};
// The bulk of the content in this view is driven by a YapDB view/mapping.
// However, we also want to optionally include ReminderView's at the top
// and an "Archived Conversations" button at the bottom. Rather than introduce
// index-offsets into the Mapping calculation, we introduce two pseudo groups
// to add a top and bottom section to the content, and create cells for those
// sections without consulting the YapMapping.
// This is a bit of a hack, but it consolidates the hacks into the Reminder/Archive section
// and allows us to leaves the bulk of the content logic on the happy path.
NSString *const kReminderViewPseudoGroup = @"kReminderViewPseudoGroup";
NSString *const kArchiveButtonPseudoGroup = @"kArchiveButtonPseudoGroup";
typedef NS_ENUM(NSInteger, HomeViewControllerSection) {
HomeViewControllerSectionReminders,
HomeViewControllerSectionConversations,
HomeViewControllerSectionArchiveButton,
};
2018-06-11 18:15:46 +02:00
@interface HomeViewController () <UITableViewDelegate,
UITableViewDataSource,
UIViewControllerPreviewingDelegate,
UISearchBarDelegate,
ConversationSearchViewDelegate,
OWSBlockListCacheDelegate>
@property (nonatomic) UITableView *tableView;
2019-02-18 17:44:27 +01:00
@property (nonatomic) UIView *emptyInboxView;
2019-02-19 15:52:10 +01:00
@property (nonatomic) UIView *firstConversationCueView;
2019-02-19 15:52:10 +01:00
@property (nonatomic) UILabel *firstConversationLabel;
@property (nonatomic) YapDatabaseConnection *editingDbConnection;
@property (nonatomic) YapDatabaseConnection *uiDatabaseConnection;
@property (nonatomic) YapDatabaseViewMappings *threadMappings;
@property (nonatomic) HomeViewMode homeViewMode;
@property (nonatomic) id previewingContext;
@property (nonatomic, readonly) NSCache<NSString *, ThreadViewModel *> *threadViewModelCache;
@property (nonatomic) BOOL isViewVisible;
@property (nonatomic) BOOL shouldObserveDBModifications;
2018-08-07 23:36:34 +02:00
@property (nonatomic) BOOL hasEverAppeared;
// Mark: Search
@property (nonatomic, readonly) UISearchBar *searchBar;
@property (nonatomic) ConversationSearchViewController *searchResultsController;
// Dependencies
@property (nonatomic, readonly) AccountManager *accountManager;
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
2016-09-21 14:37:51 +02:00
@property (nonatomic, readonly) OWSContactsManager *contactsManager;
@property (nonatomic, readonly) OWSMessageSender *messageSender;
@property (nonatomic, readonly) OWSBlockListCache *blocklistCache;
2017-05-05 18:39:21 +02:00
// Views
@property (nonatomic, readonly) UIStackView *reminderStackView;
@property (nonatomic, readonly) UITableViewCell *reminderViewCell;
@property (nonatomic, readonly) UIView *deregisteredView;
@property (nonatomic, readonly) UIView *outageView;
@property (nonatomic, readonly) UIView *archiveReminderView;
@property (nonatomic, readonly) UIView *missingContactsPermissionView;
@property (nonatomic) TSThread *lastThread;
2014-10-29 21:58:58 +01:00
@property (nonatomic) BOOL hasArchivedThreadsRow;
2018-07-23 21:03:07 +02:00
@property (nonatomic) BOOL hasThemeChanged;
@property (nonatomic) BOOL hasVisibleReminders;
2014-10-29 21:58:58 +01:00
@end
#pragma mark -
2017-09-06 19:59:39 +02:00
@implementation HomeViewController
2014-10-29 21:58:58 +01:00
2017-05-05 18:39:21 +02:00
#pragma mark - Init
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
2016-09-21 14:37:51 +02:00
- (instancetype)init
{
self = [super init];
if (!self) {
return self;
}
_homeViewMode = HomeViewMode_Inbox;
[self commonInit];
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
2016-09-21 14:37:51 +02:00
return self;
}
2018-06-12 17:27:32 +02:00
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
2016-09-21 14:37:51 +02:00
{
2018-08-27 16:29:51 +02:00
OWSFailDebug(@"Do not load this from the storyboard.");
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
2016-09-21 14:37:51 +02:00
self = [super initWithCoder:aDecoder];
if (!self) {
return self;
}
[self commonInit];
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
2016-09-21 14:37:51 +02:00
return self;
}
- (void)commonInit
{
2018-10-15 20:58:15 +02:00
_accountManager = AppEnvironment.shared.accountManager;
_contactsManager = Environment.shared.contactsManager;
2018-09-17 15:27:58 +02:00
_messageSender = SSKEnvironment.shared.messageSender;
_blocklistCache = [OWSBlockListCache new];
[_blocklistCache startObservingAndSyncStateWithDelegate:self];
_threadViewModelCache = [NSCache new];
2017-12-07 20:31:00 +01:00
// Ensure ExperienceUpgradeFinder has been initialized.
2018-05-11 16:36:40 +02:00
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-result"
[ExperienceUpgradeFinder sharedManager];
2018-05-11 16:36:40 +02:00
#pragma GCC diagnostic pop
2018-09-21 17:37:40 +02:00
}
2018-09-21 17:37:40 +02:00
- (void)observeNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(signalAccountsDidChange:)
name:OWSContactsManagerSignalAccountsDidChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillEnterForeground:)
name:OWSApplicationWillEnterForegroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidBecomeActive:)
name:OWSApplicationDidBecomeActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillResignActive:)
name:OWSApplicationWillResignActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(yapDatabaseModified:)
name:YapDatabaseModifiedNotification
object:OWSPrimaryStorage.sharedManager.dbNotificationObject];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(yapDatabaseModifiedExternally:)
name:YapDatabaseModifiedExternallyNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(registrationStateDidChange:)
name:RegistrationStateDidChangeNotification
object:nil];
2018-06-19 18:09:44 +02:00
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(outageStateDidChange:)
name:OutageDetection.outageStateDidChange
object:nil];
2018-07-12 23:45:29 +02:00
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(themeDidChange:)
2018-07-23 21:03:07 +02:00
name:ThemeDidChangeNotification
2018-07-12 23:45:29 +02:00
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(localProfileDidChange:)
name:kNSNotificationName_LocalProfileDidChange
object:nil];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
2017-05-05 18:39:21 +02:00
#pragma mark - Notifications
- (void)signalAccountsDidChange:(id)notification
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
2018-04-21 21:22:40 +02:00
[self reloadTableViewData];
2019-02-19 15:52:10 +01:00
if (!self.firstConversationCueView.isHidden) {
[self updateFirstConversationLabel];
}
}
- (void)registrationStateDidChange:(id)notification
{
OWSAssertIsOnMainThread();
2018-06-15 19:39:35 +02:00
[self updateReminderViews];
}
2018-06-19 18:09:44 +02:00
- (void)outageStateDidChange:(id)notification
{
OWSAssertIsOnMainThread();
[self updateReminderViews];
}
- (void)localProfileDidChange:(id)notification
{
OWSAssertIsOnMainThread();
[self updateBarButtonItems];
}
2018-07-13 00:01:43 +02:00
#pragma mark - Theme
2018-08-08 21:49:22 +02:00
- (void)themeDidChange:(NSNotification *)notification
2018-07-12 23:45:29 +02:00
{
OWSAssertIsOnMainThread();
[self applyTheme];
[self.tableView reloadData];
2018-07-23 21:03:07 +02:00
self.hasThemeChanged = YES;
2018-07-12 23:45:29 +02:00
}
- (void)applyTheme
{
OWSAssertIsOnMainThread();
OWSAssertDebug(self.tableView);
OWSAssertDebug(self.searchBar);
2018-07-12 23:45:29 +02:00
2018-07-13 15:50:49 +02:00
self.view.backgroundColor = Theme.backgroundColor;
self.tableView.backgroundColor = Theme.backgroundColor;
2018-07-12 23:45:29 +02:00
}
2017-05-05 18:39:21 +02:00
#pragma mark - View Life Cycle
- (void)loadView
Disappearing Messages * Per thread settings menu accessed by tapping on thread title This removed the toggle-phone behavior. You'll be able to see the phone number in the settings table view. This removed the "add contact" functionality, although it was already broken for ios>=9 (which is basically everybody). The group actions menu was absorbed into this screen * Added a confirm alert to leave group (fixes #938) * New Translation Strings * Extend "Add People" label to fit translations. * resolved issues with translations not fitting in group menu * Fix the long standing type warning where TSCalls were assigned to a TSMessageAdapter. * Can delete info messages Follow the JSQMVC pattern and put UIResponder-able content in the messageBubbleContainer. This gives us more functionality *and* allows us to delete some code. yay! It's still not yet possible to delete phone messages. =( * Fixed some compiler warnings. * xcode8 touching storyboard. So long xcode7! * Fixup multiline info messages. We were seeing info messages like "You set disappearing message timer to 10" instead of "You set disappearing message timer to 10 seconds." Admittedly this isn't a very good fix, as now one liners feel like they have too much padding. If the message is well over one line, we were wrapping properly, but there's a problem when the message is *just barely* two lines, the cell height grows, but the label still thinks it's just one line (as evinced by the one line appearing in the center of the label frame. The result being that the last word of the label is cropped. * Disable group actions after leaving group. // FREEBIE
2016-09-21 14:37:51 +02:00
{
[super loadView];
// TODO: Remove this.
if (self.homeViewMode == HomeViewMode_Inbox) {
2019-11-28 06:42:07 +01:00
// [SignalApp.sharedApp setHomeViewController:self];
}
2018-06-20 21:15:33 +02:00
UIStackView *reminderStackView = [UIStackView new];
_reminderStackView = reminderStackView;
2018-06-20 21:15:33 +02:00
reminderStackView.axis = UILayoutConstraintAxisVertical;
reminderStackView.spacing = 0;
_reminderViewCell = [UITableViewCell new];
self.reminderViewCell.selectionStyle = UITableViewCellSelectionStyleNone;
[self.reminderViewCell.contentView addSubview:reminderStackView];
[reminderStackView autoPinEdgesToSuperviewEdges];
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _reminderViewCell);
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, reminderStackView);
2018-06-18 16:54:06 +02:00
__weak HomeViewController *weakSelf = self;
2018-06-15 19:39:35 +02:00
ReminderView *deregisteredView =
2018-06-18 16:54:06 +02:00
[ReminderView nagWithText:NSLocalizedString(@"DEREGISTRATION_WARNING",
2018-06-15 19:39:35 +02:00
@"Label warning the user that they have been de-registered.")
tapAction:^{
2018-06-18 17:28:21 +02:00
HomeViewController *strongSelf = weakSelf;
if (!strongSelf) {
return;
}
[RegistrationUtils showReregistrationUIFromViewController:strongSelf];
2018-06-15 19:39:35 +02:00
}];
_deregisteredView = deregisteredView;
2018-06-20 21:15:33 +02:00
[reminderStackView addArrangedSubview:deregisteredView];
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, deregisteredView);
2018-06-15 19:39:35 +02:00
2018-06-19 18:09:44 +02:00
ReminderView *outageView = [ReminderView
nagWithText:NSLocalizedString(@"OUTAGE_WARNING", @"Label warning the user that the Signal service may be down.")
tapAction:nil];
_outageView = outageView;
2018-06-19 18:09:44 +02:00
[reminderStackView addArrangedSubview:outageView];
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, outageView);
2018-06-19 18:09:44 +02:00
ReminderView *archiveReminderView =
[ReminderView explanationWithText:NSLocalizedString(@"INBOX_VIEW_ARCHIVE_MODE_REMINDER",
@"Label reminding the user that they are in archive mode.")];
_archiveReminderView = archiveReminderView;
2018-06-20 21:15:33 +02:00
[reminderStackView addArrangedSubview:archiveReminderView];
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, archiveReminderView);
ReminderView *missingContactsPermissionView = [ReminderView
nagWithText:NSLocalizedString(@"INBOX_VIEW_MISSING_CONTACTS_PERMISSION",
@"Multi-line label explaining how to show names instead of phone numbers in your inbox")
tapAction:^{
[[UIApplication sharedApplication] openSystemSettings];
}];
_missingContactsPermissionView = missingContactsPermissionView;
2018-06-20 21:15:33 +02:00
[reminderStackView addArrangedSubview:missingContactsPermissionView];
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, missingContactsPermissionView);
self.tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
2018-08-22 22:30:12 +02:00
self.tableView.separatorColor = Theme.cellSeparatorColor;
2018-04-10 19:02:33 +02:00
[self.tableView registerClass:[HomeViewCell class] forCellReuseIdentifier:HomeViewCell.cellReuseIdentifier];
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kArchivedConversationsReuseIdentifier];
[self.view addSubview:self.tableView];
[self.tableView autoPinEdgesToSuperviewEdges];
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _tableView);
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _searchBar);
2018-06-30 00:49:24 +02:00
2018-06-12 17:27:32 +02:00
self.tableView.rowHeight = UITableViewAutomaticDimension;
2018-06-15 17:08:01 +02:00
self.tableView.estimatedRowHeight = 60;
self.emptyInboxView = [self createEmptyInboxView];
[self.view addSubview:self.emptyInboxView];
[self.emptyInboxView autoPinWidthToSuperviewMargins];
[self.emptyInboxView autoVCenterInSuperview];
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _emptyInboxView);
2019-02-19 15:52:10 +01:00
[self createFirstConversationCueView];
[self.view addSubview:self.firstConversationCueView];
2019-02-19 15:52:10 +01:00
[self.firstConversationCueView autoPinToTopLayoutGuideOfViewController:self withInset:0.f];
2019-02-21 15:53:49 +01:00
// This inset bakes in assumptions about UINavigationBar layout, but I'm not sure
// there's a better way to do it, since it isn't safe to use iOS auto layout with
// UINavigationBar contents.
[self.firstConversationCueView autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:6.f];
2019-02-19 15:52:10 +01:00
[self.firstConversationCueView autoPinEdgeToSuperviewEdge:ALEdgeLeading
withInset:10
relation:NSLayoutRelationGreaterThanOrEqual];
[self.firstConversationCueView autoPinEdgeToSuperviewMargin:ALEdgeBottom
relation:NSLayoutRelationGreaterThanOrEqual];
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _firstConversationCueView);
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _firstConversationLabel);
UIRefreshControl *pullToRefreshView = [UIRefreshControl new];
pullToRefreshView.tintColor = [UIColor grayColor];
[pullToRefreshView addTarget:self
action:@selector(pullToRefreshPerformed:)
forControlEvents:UIControlEventValueChanged];
[self.tableView insertSubview:pullToRefreshView atIndex:0];
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, pullToRefreshView);
}
- (UIView *)createEmptyInboxView
{
/*
2019-02-18 17:44:27 +01:00
NSArray<NSString *> *emptyInboxImageNames = @[
@"home_empty_splash_1",
@"home_empty_splash_2",
@"home_empty_splash_3",
@"home_empty_splash_4",
@"home_empty_splash_5",
];
NSString *emptyInboxImageName = emptyInboxImageNames[arc4random_uniform((uint32_t) emptyInboxImageNames.count)];
UIImageView *emptyInboxImageView = [UIImageView new];
emptyInboxImageView.image = [UIImage imageNamed:emptyInboxImageName];
emptyInboxImageView.layer.minificationFilter = kCAFilterTrilinear;
emptyInboxImageView.layer.magnificationFilter = kCAFilterTrilinear;
[emptyInboxImageView autoPinToAspectRatioWithSize:emptyInboxImageView.image.size];
2019-03-01 20:02:43 +01:00
CGSize screenSize = UIScreen.mainScreen.bounds.size;
CGFloat emptyInboxImageSize = MIN(screenSize.width, screenSize.height) * 0.65f;
[emptyInboxImageView autoSetDimension:ALDimensionWidth toSize:emptyInboxImageSize];
*/
2019-03-01 20:02:43 +01:00
2019-02-18 17:44:27 +01:00
UILabel *emptyInboxLabel = [UILabel new];
2019-07-25 05:48:39 +02:00
emptyInboxLabel.text = NSLocalizedString(@"Looks like you don't have any conversations yet. Get started by messaging a friend.", @"");
2019-02-19 15:52:10 +01:00
emptyInboxLabel.font = UIFont.ows_dynamicTypeBodyClampedFont;
2019-07-25 05:48:39 +02:00
emptyInboxLabel.textColor = UIColor.whiteColor;
2019-02-18 17:44:27 +01:00
emptyInboxLabel.textAlignment = NSTextAlignmentCenter;
emptyInboxLabel.numberOfLines = 0;
emptyInboxLabel.lineBreakMode = NSLineBreakByWordWrapping;
UIStackView *emptyInboxStack = [[UIStackView alloc] initWithArrangedSubviews:@[
/*emptyInboxImageView,*/
2019-02-18 17:44:27 +01:00
emptyInboxLabel,
]];
emptyInboxStack.axis = UILayoutConstraintAxisVertical;
emptyInboxStack.alignment = UIStackViewAlignmentCenter;
emptyInboxStack.spacing = 12;
emptyInboxStack.layoutMargins = UIEdgeInsetsMake(50, 50, 50, 50);
emptyInboxStack.layoutMarginsRelativeArrangement = YES;
return emptyInboxStack;
}
2019-02-19 15:52:10 +01:00
- (void)createFirstConversationCueView
{
2019-02-19 15:52:10 +01:00
const CGFloat kTailWidth = 16.f;
const CGFloat kTailHeight = 8.f;
const CGFloat kTailHMargin = 12.f;
2019-02-19 15:52:10 +01:00
UILabel *label = [UILabel new];
label.textColor = UIColor.ows_whiteColor;
label.font = UIFont.ows_dynamicTypeBodyClampedFont;
label.numberOfLines = 0;
label.lineBreakMode = NSLineBreakByWordWrapping;
OWSLayerView *layerView = [OWSLayerView new];
2019-02-21 15:53:49 +01:00
layerView.layoutMargins = UIEdgeInsetsMake(11 + kTailHeight, 16, 11, 16);
2019-02-19 15:52:10 +01:00
CAShapeLayer *shapeLayer = [CAShapeLayer new];
2019-06-14 08:36:40 +02:00
shapeLayer.fillColor = UIColor.lokiGreen.CGColor;
2019-02-19 15:52:10 +01:00
[layerView.layer addSublayer:shapeLayer];
layerView.layoutCallback = ^(UIView *view) {
UIBezierPath *bezierPath = [UIBezierPath new];
// Bubble
CGRect bubbleBounds = view.bounds;
bubbleBounds.origin.y += kTailHeight;
bubbleBounds.size.height -= kTailHeight;
[bezierPath appendPath:[UIBezierPath bezierPathWithRoundedRect:bubbleBounds cornerRadius:8]];
// Tail
CGPoint tailTop = CGPointMake(kTailHMargin + kTailWidth * 0.5f, 0.f);
CGPoint tailLeft = CGPointMake(kTailHMargin, kTailHeight);
CGPoint tailRight = CGPointMake(kTailHMargin + kTailWidth, kTailHeight);
if (!CurrentAppContext().isRTL) {
tailTop.x = view.width - tailTop.x;
tailLeft.x = view.width - tailLeft.x;
tailRight.x = view.width - tailRight.x;
}
[bezierPath moveToPoint:tailTop];
[bezierPath addLineToPoint:tailLeft];
[bezierPath addLineToPoint:tailRight];
[bezierPath addLineToPoint:tailTop];
shapeLayer.path = bezierPath.CGPath;
shapeLayer.frame = view.bounds;
};
[layerView addSubview:label];
[label autoPinEdgesToSuperviewMargins];
2019-02-19 15:52:10 +01:00
2019-02-21 15:53:49 +01:00
layerView.userInteractionEnabled = YES;
[layerView
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(firstConversationCueWasTapped:)]];
2019-02-19 15:52:10 +01:00
self.firstConversationCueView = layerView;
self.firstConversationLabel = label;
}
2019-02-21 15:53:49 +01:00
- (void)firstConversationCueWasTapped:(UITapGestureRecognizer *)gestureRecognizer
{
OWSLogInfo(@"");
AppPreferences.hasDimissedFirstConversationCue = YES;
[self updateViewState];
}
2019-03-07 21:13:56 +01:00
- (NSArray<SignalAccount *> *)suggestedAccountsForFirstContact
{
NSMutableArray<SignalAccount *> *accounts = [NSMutableArray new];
NSString *_Nullable localNumber = [TSAccountManager localNumber];
if (localNumber == nil) {
OWSFailDebug(@"localNumber was unexepectedly nil");
return @[];
}
for (SignalAccount *account in self.contactsManager.signalAccounts) {
if ([localNumber isEqual:account.recipientId]) {
continue;
}
if (accounts.count >= 3) {
2019-03-11 17:47:49 +01:00
break;
2019-03-07 21:13:56 +01:00
}
2019-03-11 17:47:49 +01:00
[accounts addObject:account];
2019-03-07 21:13:56 +01:00
}
return [accounts copy];
}
2019-02-19 15:52:10 +01:00
- (void)updateFirstConversationLabel
{
2019-03-07 21:13:56 +01:00
NSArray<SignalAccount *> *signalAccounts = self.suggestedAccountsForFirstContact;
2019-02-19 15:52:10 +01:00
NSString *formatString = @"";
NSMutableArray<NSString *> *contactNames = [NSMutableArray new];
if (signalAccounts.count >= 3) {
[contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[0]]];
[contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[1]]];
[contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[2]]];
formatString = NSLocalizedString(@"HOME_VIEW_FIRST_CONVERSATION_OFFER_3_CONTACTS_FORMAT",
@"Format string for a label offering to start a new conversation with your contacts, if you have at least "
@"3 Signal contacts. Embeds {{The names of 3 of your Signal contacts}}.");
2019-02-19 19:41:28 +01:00
} else if (signalAccounts.count == 2) {
2019-02-19 15:52:10 +01:00
[contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[0]]];
[contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[1]]];
formatString = NSLocalizedString(@"HOME_VIEW_FIRST_CONVERSATION_OFFER_2_CONTACTS_FORMAT",
@"Format string for a label offering to start a new conversation with your contacts, if you have 2 Signal "
@"contacts. Embeds {{The names of 2 of your Signal contacts}}.");
2019-02-19 19:41:28 +01:00
} else if (signalAccounts.count == 1) {
2019-02-19 15:52:10 +01:00
[contactNames addObject:[self.contactsManager displayNameForSignalAccount:signalAccounts[0]]];
formatString = NSLocalizedString(@"HOME_VIEW_FIRST_CONVERSATION_OFFER_1_CONTACT_FORMAT",
@"Format string for a label offering to start a new conversation with your contacts, if you have 1 Signal "
@"contact. Embeds {{The name of 1 of your Signal contacts}}.");
}
2019-02-19 15:52:10 +01:00
NSString *embedToken = @"%@";
NSArray<NSString *> *formatSplits = [formatString componentsSeparatedByString:embedToken];
// We need to use a complicated format string that possibly embeds multiple contact names.
// Translator error could easily lead to an invalid format string.
// We need to verify that it was translated properly.
BOOL isValidFormatString = (contactNames.count > 0 && formatSplits.count == contactNames.count + 1);
for (NSString *contactName in contactNames) {
if ([contactName containsString:embedToken]) {
isValidFormatString = NO;
}
}
NSMutableAttributedString *_Nullable attributedString = nil;
if (isValidFormatString) {
attributedString = [[NSMutableAttributedString alloc] initWithString:formatString];
while (contactNames.count > 0) {
NSString *contactName = contactNames.firstObject;
[contactNames removeObjectAtIndex:0];
NSRange range = [attributedString.string rangeOfString:embedToken];
if (range.location == NSNotFound) {
// Error
attributedString = nil;
break;
}
NSAttributedString *formattedName = [[NSAttributedString alloc]
initWithString:contactName
attributes:@{
NSFontAttributeName : self.firstConversationLabel.font.ows_mediumWeight,
}];
[attributedString replaceCharactersInRange:range withAttributedString:formattedName];
}
}
if (!attributedString) {
// The default case handles the no-contacts scenario and all error cases.
NSString *defaultText = NSLocalizedString(@"HOME_VIEW_FIRST_CONVERSATION_OFFER_NO_CONTACTS",
@"A label offering to start a new conversation with your contacts, if you have no Signal contacts.");
attributedString = [[NSMutableAttributedString alloc] initWithString:defaultText];
}
self.firstConversationLabel.attributedText = [attributedString copy];
}
- (void)updateReminderViews
{
self.archiveReminderView.hidden = self.homeViewMode != HomeViewMode_Archive;
2018-07-16 21:51:00 +02:00
// App is killed and restarted when the user changes their contact permissions, so need need to "observe" anything
// to re-render this.
self.missingContactsPermissionView.hidden = !self.contactsManager.isSystemContactsDenied;
self.deregisteredView.hidden = !TSAccountManager.sharedInstance.isDeregistered;
self.outageView.hidden = !OutageDetection.sharedManager.hasOutage;
self.hasVisibleReminders = !self.archiveReminderView.isHidden || !self.missingContactsPermissionView.isHidden
|| !self.deregisteredView.isHidden || !self.outageView.isHidden;
}
2018-09-21 17:37:40 +02:00
- (void)setHasVisibleReminders:(BOOL)hasVisibleReminders
{
if (_hasVisibleReminders == hasVisibleReminders) {
return;
}
_hasVisibleReminders = hasVisibleReminders;
// If the reminders show/hide, reload the table.
[self.tableView reloadData];
}
2017-09-06 19:59:39 +02:00
- (void)viewDidLoad
{
2014-10-29 21:58:58 +01:00
[super viewDidLoad];
self.editingDbConnection = OWSPrimaryStorage.sharedManager.newDatabaseConnection;
// Create the database connection.
[self uiDatabaseConnection];
[self updateMappings];
[self updateViewState];
[self updateReminderViews];
2018-09-21 17:37:40 +02:00
[self observeNotifications];
// because this uses the table data source, `tableViewSetup` must happen
// after mappings have been set up in `showInboxGrouping`
[self tableViewSetUp];
switch (self.homeViewMode) {
case HomeViewMode_Inbox:
// TODO: Should our app name be translated? Probably not.
2019-06-14 07:25:39 +02:00
self.title = NSLocalizedString(@"Loki Messenger", @"");
break;
case HomeViewMode_Archive:
self.title = NSLocalizedString(@"HOME_VIEW_TITLE_ARCHIVE", @"Title for the home view's 'archive' mode.");
break;
}
[self applyDefaultBackButton];
if ([self.traitCollection respondsToSelector:@selector(forceTouchCapability)]
&& (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable)) {
[self registerForPreviewingWithDelegate:self sourceView:self.tableView];
}
// Search
2018-06-11 18:15:46 +02:00
UISearchBar *searchBar = [OWSSearchBar new];
2018-06-11 18:15:46 +02:00
_searchBar = searchBar;
searchBar.placeholder = NSLocalizedString(@"HOME_VIEW_CONVERSATION_SEARCHBAR_PLACEHOLDER",
@"Placeholder text for search bar which filters conversations.");
searchBar.delegate = self;
[searchBar sizeToFit];
// Setting tableHeader calls numberOfSections, which must happen after updateMappings has been called at least once.
OWSAssertDebug(self.tableView.tableHeaderView == nil);
2018-06-11 18:15:46 +02:00
self.tableView.tableHeaderView = self.searchBar;
2018-08-16 17:42:31 +02:00
// Hide search bar by default. User can pull down to search.
self.tableView.contentOffset = CGPointMake(0, CGRectGetHeight(searchBar.frame));
2018-06-11 18:15:46 +02:00
ConversationSearchViewController *searchResultsController = [ConversationSearchViewController new];
searchResultsController.delegate = self;
self.searchResultsController = searchResultsController;
2018-06-11 18:15:46 +02:00
[self addChildViewController:searchResultsController];
[self.view addSubview:searchResultsController.view];
2019-01-08 16:44:22 +01:00
[searchResultsController.view autoPinEdgeToSuperviewEdge:ALEdgeBottom];
[searchResultsController.view autoPinEdgeToSuperviewSafeArea:ALEdgeLeading];
[searchResultsController.view autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing];
2018-07-31 22:28:41 +02:00
if (@available(iOS 11, *)) {
[searchResultsController.view autoPinTopToSuperviewMarginWithInset:56];
} else {
[searchResultsController.view autoPinToTopLayoutGuideOfViewController:self withInset:40];
}
2018-06-13 19:35:55 +02:00
searchResultsController.view.hidden = YES;
2018-06-11 18:15:46 +02:00
2018-09-21 17:37:40 +02:00
[self updateReminderViews];
[self updateBarButtonItems];
2018-07-23 21:03:07 +02:00
[self applyTheme];
2019-08-01 06:45:34 +02:00
NSString *buildNumberAsString = [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"];
NSInteger buildNumber = buildNumberAsString.integerValue;
BOOL didUpdateForMainnet = [NSUserDefaults.standardUserDefaults boolForKey:@"didUpdateForMainnet"];
2019-08-02 03:17:14 +02:00
if ((buildNumber == 8 || buildNumber == 9 || buildNumber == 10 || buildNumber == 11) && !didUpdateForMainnet) {
2019-08-01 06:45:34 +02:00
NSString *title = NSLocalizedString(@"Update Required", @"");
NSString *message = NSLocalizedString(@"This version of Loki Messenger is no longer supported. Please press OK to reset your account and migrate to the latest version.", @"");
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
[ThreadUtil deleteAllContent];
[SSKEnvironment.shared.identityManager clearIdentityKey];
2020-05-28 03:38:44 +02:00
[LKAPI clearSnodePool];
2019-08-29 07:21:45 +02:00
AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
2020-03-25 00:27:43 +01:00
[appDelegate stopPollerIfNeeded];
2020-02-21 04:40:44 +01:00
[appDelegate stopOpenGroupPollersIfNeeded];
2019-08-02 03:16:34 +02:00
[SSKEnvironment.shared.tsAccountManager resetForReregistration];
2019-08-01 06:45:34 +02:00
UIViewController *rootViewController = [[OnboardingController new] initialViewController];
OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:rootViewController];
navigationController.navigationBarHidden = YES;
UIApplication.sharedApplication.keyWindow.rootViewController = navigationController;
}]];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { /* Do nothing */ }]];
[self presentAlert:alert];
}
if (OWSIdentityManager.sharedManager.identityKeyPair != nil) {
AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
2019-10-15 01:50:06 +02:00
[appDelegate setUpDefaultPublicChatsIfNeeded];
2019-08-27 08:48:53 +02:00
[appDelegate createRSSFeedsIfNeeded];
2019-10-15 01:50:06 +02:00
[LKPublicChatManager.shared startPollersIfNeeded];
2019-08-27 08:48:53 +02:00
[appDelegate startRSSFeedPollersIfNeeded];
}
}
- (void)applyDefaultBackButton
{
// We don't show any text for the back button, so there's no need to localize it. But because we left align the
// conversation title view, we add a little tappable padding after the back button, by having a title of spaces.
// Admittedly this is kind of a hack and not super fine grained, but it's simple and results in the interactive pop
// gesture animating our title view nicely vs. creating our own back button bar item with custom padding, which does
// not properly animate with the "swipe to go back" or "swipe left for info" gestures.
NSUInteger paddingLength = 3;
NSString *paddingString = [@"" stringByPaddingToLength:paddingLength withString:@" " startingAtIndex:0];
2018-04-24 17:42:04 +02:00
self.navigationItem.backBarButtonItem =
[[UIBarButtonItem alloc] initWithTitle:paddingString style:UIBarButtonItemStylePlain target:nil action:nil];
}
- (void)applyArchiveBackButton
{
self.navigationItem.backBarButtonItem =
[[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"BACK_BUTTON", @"button text for back button")
style:UIBarButtonItemStylePlain
target:nil
action:nil];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self displayAnyUnseenUpgradeExperience];
[self applyDefaultBackButton];
2018-07-23 21:03:07 +02:00
if (self.hasThemeChanged) {
[self.tableView reloadData];
self.hasThemeChanged = NO;
}
2018-08-07 23:36:34 +02:00
[self requestReviewIfAppropriate];
2018-07-23 21:03:07 +02:00
[self.searchResultsController viewDidAppear:animated];
2018-08-07 23:36:34 +02:00
self.hasEverAppeared = YES;
2018-07-23 21:03:07 +02:00
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[self.searchResultsController viewDidDisappear:animated];
}
2017-09-06 19:59:39 +02:00
- (void)updateBarButtonItems
{
if (self.homeViewMode != HomeViewMode_Inbox) {
return;
}
// Settings button.
UIBarButtonItem *settingsButton;
2018-10-09 18:20:46 +02:00
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(11, 0)) {
const NSUInteger kAvatarSize = 28;
NSString *masterDeviceHexEncodedPublicKey = [NSUserDefaults.standardUserDefaults stringForKey:@"masterDeviceHexEncodedPublicKey"];
NSString *hexEncodedPublicKey = masterDeviceHexEncodedPublicKey ?: TSAccountManager.localNumber;
UIImage *_Nullable localProfileAvatarImage = [OWSProfileManager.sharedManager profileAvatarForRecipientId:hexEncodedPublicKey];
UIImage *avatarImage = (localProfileAvatarImage
?: [[[OWSContactAvatarBuilder alloc] initForLocalUserWithDiameter:kAvatarSize] buildDefaultImage]);
OWSAssertDebug(avatarImage);
UIButton *avatarButton = [AvatarImageButton buttonWithType:UIButtonTypeCustom];
[avatarButton addTarget:self
action:@selector(settingsButtonPressed:)
forControlEvents:UIControlEventTouchUpInside];
[avatarButton setImage:avatarImage forState:UIControlStateNormal];
[avatarButton autoSetDimension:ALDimensionWidth toSize:kAvatarSize];
[avatarButton autoSetDimension:ALDimensionHeight toSize:kAvatarSize];
settingsButton = [[UIBarButtonItem alloc] initWithCustomView:avatarButton];
} else {
// iOS 9 and 10 have a bug around layout of custom views in UIBarButtonItem,
// so we just use a simple icon.
UIImage *image = [UIImage imageNamed:@"button_settings_white"];
settingsButton = [[UIBarButtonItem alloc] initWithImage:image
style:UIBarButtonItemStylePlain
target:self
action:@selector(settingsButtonPressed:)
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"settings")];
}
settingsButton.accessibilityLabel = CommonStrings.openSettingsButton;
self.navigationItem.leftBarButtonItem = settingsButton;
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, settingsButton);
2019-10-14 05:40:18 +02:00
UIBarButtonItem *newPrivateChatButton = [[UIBarButtonItem alloc]
2019-10-09 07:19:58 +02:00
initWithBarButtonSystemItem:UIBarButtonSystemItemCompose
target:self
2019-10-14 05:40:18 +02:00
action:@selector(showNewConversationVC)
2019-10-09 07:19:58 +02:00
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"compose")];
2019-10-14 05:40:18 +02:00
UIBarButtonItem *newGroupChatButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"group-avatar"] style:UIBarButtonItemStylePlain target:self action:@selector(showNewPublicChatVC)];
2019-10-09 07:19:58 +02:00
2019-10-14 05:40:18 +02:00
self.navigationItem.rightBarButtonItems = @[ newPrivateChatButton, newGroupChatButton ];
}
2018-04-10 19:02:33 +02:00
- (void)settingsButtonPressed:(id)sender
{
OWSNavigationController *navigationController = [AppSettingsViewController inModalNavigationController];
2017-05-23 16:25:47 +02:00
[self presentViewController:navigationController animated:YES completion:nil];
}
2018-06-12 17:27:32 +02:00
- (nullable UIViewController *)previewingContext:(id<UIViewControllerPreviewing>)previewingContext
viewControllerForLocation:(CGPoint)location
2017-09-06 19:59:39 +02:00
{
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:location];
if (!indexPath) {
2018-05-14 17:47:13 +02:00
return nil;
}
if (indexPath.section != HomeViewControllerSectionConversations) {
return nil;
}
[previewingContext setSourceRect:[self.tableView rectForRowAtIndexPath:indexPath]];
ConversationViewController *vc = [ConversationViewController new];
TSThread *thread = [self threadForIndexPath:indexPath];
self.lastThread = thread;
[vc configureForThread:thread action:ConversationViewActionNone focusMessageId:nil];
[vc peekSetup];
return vc;
}
- (void)previewingContext:(id<UIViewControllerPreviewing>)previewingContext
2017-09-06 19:59:39 +02:00
commitViewController:(UIViewController *)viewControllerToCommit
{
2017-09-06 20:13:18 +02:00
ConversationViewController *vc = (ConversationViewController *)viewControllerToCommit;
[vc popped];
[self.navigationController pushViewController:vc animated:NO];
}
2019-10-14 05:40:18 +02:00
- (void)showNewConversationVC
{
// LKNewConversationVC *newConversationVC = [LKNewConversationVC new];
// OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:newConversationVC];
// [self.navigationController presentViewController:navigationController animated:YES completion:nil];
2019-06-17 08:20:09 +02:00
/**
2018-04-23 20:13:55 +02:00
OWSAssertIsOnMainThread();
OWSLogInfo(@"");
2018-04-23 20:13:55 +02:00
NewContactThreadViewController *viewController = [NewContactThreadViewController new];
[self.contactsManager requestSystemContactsOnceWithCompletion:^(NSError *_Nullable error) {
if (error) {
OWSLogError(@"Error when requesting contacts: %@", error);
}
// Even if there is an error fetching contacts we proceed to the next screen.
// As the compose view will present the proper thing depending on contact access.
//
// We just want to make sure contact access is *complete* before showing the compose
// screen to avoid flicker.
Faster conversation presentation. There are multiple places in the codebase we present a conversation. We used to have some very conservative machinery around how this was done, for fear of failing to present the call view controller, which would have left a hidden call in the background. We've since addressed that concern more thoroughly via the separate calling UIWindow. As such, the remaining presentation machinery is overly complex and inflexible for what we need. Sometimes we want to animate-push the conversation. (tap on home, tap on "send message" in contact card/group members) Sometimes we want to dismiss a modal, to reveal the conversation behind it (contact picker, group creation) Sometimes we want to present the conversation with no animation (becoming active from a notification) We also want to ensure that we're never pushing more than one conversation view controller, which was previously a problem since we were "pushing" a newly constructed VC in response to these myriad actions. It turned out there were certain code paths that caused multiple actions to be fired in rapid succession which pushed multiple ConversationVC's. The built-in method: `setViewControllers:animated` easily ensures we only have one ConversationVC on the stack, while being composable enough to faciliate the various more efficient animations we desire. The only thing lost with the complex methods is that the naive `presentViewController:` can fail, e.g. if another view is already presented. E.g. if an alert appears *just* before the user taps compose, the contact picker will fail to present. Since we no longer depend on this for presenting the CallViewController, this isn't catostrophic, and in fact, arguable preferable, since we want the user to read and dismiss any alert explicitly. // FREEBIE
2018-08-18 22:54:35 +02:00
OWSNavigationController *modal = [[OWSNavigationController alloc] initWithRootViewController:viewController];
[self.navigationController presentViewController:modal animated:YES completion:nil];
}];
2019-06-17 08:20:09 +02:00
*/
2015-10-31 23:13:28 +01:00
}
2019-10-14 05:40:18 +02:00
- (void)showNewPublicChatVC
2019-10-09 07:19:58 +02:00
{
// LKJoinPublicChatVC *joinPublicChatVC = [LKJoinPublicChatVC new];
// OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:joinPublicChatVC];
// [self.navigationController presentViewController:navigationController animated:YES completion:nil];
2019-10-09 07:19:58 +02:00
}
2017-09-06 19:59:39 +02:00
- (void)viewWillAppear:(BOOL)animated
{
2014-11-24 21:51:43 +01:00
[super viewWillAppear:animated];
__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];
});
}];
}
self.isViewVisible = YES;
BOOL isShowingSearchResults = !self.searchResultsController.view.hidden;
if (isShowingSearchResults) {
OWSAssertDebug(self.searchBar.text.ows_stripped.length > 0);
[self scrollSearchBarToTopAnimated:NO];
} else if (self.lastThread) {
OWSAssertDebug(self.searchBar.text.ows_stripped.length == 0);
// When returning to home view, try to ensure that the "last" thread is still
// visible. The threads often change ordering while in conversation view due
// to incoming & outgoing messages.
2017-07-11 22:11:56 +02:00
__block NSIndexPath *indexPathOfLastThread = nil;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
indexPathOfLastThread =
[[transaction extension:TSThreadDatabaseViewExtensionName] indexPathForKey:self.lastThread.uniqueId
inCollection:[TSThread collection]
withMappings:self.threadMappings];
2017-07-11 22:11:56 +02:00
}];
if (indexPathOfLastThread) {
[self.tableView scrollToRowAtIndexPath:indexPathOfLastThread
atScrollPosition:UITableViewScrollPositionNone
animated:NO];
}
}
[self updateViewState];
[self applyDefaultBackButton];
if ([self updateHasArchivedThreadsRow]) {
[self.tableView reloadData];
}
2018-07-23 21:03:07 +02:00
[self.searchResultsController viewWillAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
self.isViewVisible = NO;
2018-07-23 21:03:07 +02:00
[self.searchResultsController viewWillDisappear:animated];
}
- (void)setIsViewVisible:(BOOL)isViewVisible
{
_isViewVisible = isViewVisible;
[self updateShouldObserveDBModifications];
}
- (void)updateShouldObserveDBModifications
{
BOOL isAppForegroundAndActive = CurrentAppContext().isAppForegroundAndActive;
self.shouldObserveDBModifications = self.isViewVisible && isAppForegroundAndActive;
}
- (void)setShouldObserveDBModifications:(BOOL)shouldObserveDBModifications
{
if (_shouldObserveDBModifications == shouldObserveDBModifications) {
return;
}
_shouldObserveDBModifications = shouldObserveDBModifications;
2017-07-26 18:39:43 +02:00
if (self.shouldObserveDBModifications) {
[self resetMappings];
}
2017-07-26 18:39:43 +02:00
}
2018-04-21 21:22:40 +02:00
- (void)reloadTableViewData
{
// PERF: come up with a more nuanced cache clearing scheme
[self.threadViewModelCache removeAllObjects];
2018-04-21 21:22:40 +02:00
[self.tableView reloadData];
}
2017-07-26 18:39:43 +02:00
- (void)resetMappings
{
// If we're entering "active" mode (e.g. view is visible and app is in foreground),
// reset all state updated by yapDatabaseModified:.
if (self.threadMappings != nil) {
// Before we begin observing database modifications, make sure
// our mapping and table state is up-to-date.
//
// We need to `beginLongLivedReadTransaction` before we update our
// mapping in order to jump to the most recent commit.
[self.uiDatabaseConnection beginLongLivedReadTransaction];
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
[self.threadMappings updateWithTransaction:transaction];
}];
}
[self updateHasArchivedThreadsRow];
2018-04-21 21:22:40 +02:00
[self reloadTableViewData];
[self updateViewState];
2017-07-26 18:39:43 +02:00
// If the user hasn't already granted contact access
// we don't want to request until they receive a message.
__block BOOL hasAnyMessages;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction * _Nonnull transaction) {
hasAnyMessages = [self hasAnyMessagesWithTransaction:transaction];
}];
if (hasAnyMessages) {
2017-07-26 18:39:43 +02:00
[self.contactsManager requestSystemContactsOnce];
}
}
- (void)applicationWillEnterForeground:(NSNotification *)notification
{
[self updateViewState];
}
- (BOOL)hasAnyMessagesWithTransaction:(YapDatabaseReadTransaction *)transaction
{
return [TSThread numberOfKeysInCollectionWithTransaction:transaction] > 0;
}
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
[self updateShouldObserveDBModifications];
// 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.
__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];
});
}];
}
}
- (void)applicationWillResignActive:(NSNotification *)notification
{
[self updateShouldObserveDBModifications];
}
#pragma mark - startup
- (NSArray<ExperienceUpgrade *> *)unseenUpgradeExperiences
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
__block NSArray<ExperienceUpgrade *> *unseenUpgrades;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
2017-12-07 16:33:27 +01:00
unseenUpgrades = [ExperienceUpgradeFinder.sharedManager allUnseenWithTransaction:transaction];
}];
return unseenUpgrades;
}
- (void)displayAnyUnseenUpgradeExperience
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
NSArray<ExperienceUpgrade *> *unseenUpgrades = [self unseenUpgradeExperiences];
if (unseenUpgrades.count > 0) {
2017-09-06 19:59:39 +02:00
ExperienceUpgradesPageViewController *experienceUpgradeViewController =
[[ExperienceUpgradesPageViewController alloc] initWithExperienceUpgrades:unseenUpgrades];
[self presentViewController:experienceUpgradeViewController animated:YES completion:nil];
} else {
[OWSAlerts showIOSUpgradeNagIfNecessary];
}
}
2017-09-06 19:59:39 +02:00
- (void)tableViewSetUp
{
self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
2014-10-29 21:58:58 +01:00
}
2015-01-24 04:26:04 +01:00
#pragma mark - Table View Data Source
2014-10-29 21:58:58 +01:00
// Returns YES IFF this value changes.
- (BOOL)updateHasArchivedThreadsRow
{
BOOL hasArchivedThreadsRow = (self.homeViewMode == HomeViewMode_Inbox && self.numberOfArchivedThreads > 0);
if (self.hasArchivedThreadsRow == hasArchivedThreadsRow) {
return NO;
}
self.hasArchivedThreadsRow = hasArchivedThreadsRow;
return YES;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return (NSInteger)[self.threadMappings numberOfSections];
2014-10-29 21:58:58 +01:00
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)aSection
2017-09-06 19:59:39 +02:00
{
HomeViewControllerSection section = (HomeViewControllerSection)aSection;
switch (section) {
case HomeViewControllerSectionReminders: {
return self.hasVisibleReminders ? 1 : 0;
}
case HomeViewControllerSectionConversations: {
NSInteger result = (NSInteger)[self.threadMappings numberOfItemsInSection:(NSUInteger)section];
return result;
}
case HomeViewControllerSectionArchiveButton: {
return self.hasArchivedThreadsRow ? 1 : 0;
}
}
OWSFailDebug(@"failure: unexpected section: %lu", (unsigned long)section);
return 0;
2014-10-29 21:58:58 +01:00
}
- (ThreadViewModel *)threadViewModelForIndexPath:(NSIndexPath *)indexPath
2017-05-05 18:39:21 +02:00
{
2018-04-21 17:12:58 +02:00
TSThread *threadRecord = [self threadForIndexPath:indexPath];
OWSAssertDebug(threadRecord);
ThreadViewModel *_Nullable cachedThreadViewModel = [self.threadViewModelCache objectForKey:threadRecord.uniqueId];
if (cachedThreadViewModel) {
return cachedThreadViewModel;
}
__block ThreadViewModel *_Nullable newThreadViewModel;
2018-04-21 17:12:58 +02:00
[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
{
HomeViewControllerSection section = (HomeViewControllerSection)indexPath.section;
switch (section) {
case HomeViewControllerSectionReminders: {
2018-09-21 17:37:40 +02:00
OWSAssert(self.reminderStackView);
return self.reminderViewCell;
}
case HomeViewControllerSectionConversations: {
return [self tableView:tableView cellForConversationAtIndexPath:indexPath];
}
case HomeViewControllerSectionArchiveButton: {
return [self cellForArchivedConversationsRow:tableView];
}
}
OWSFailDebug(@"failure: unexpected section: %lu", (unsigned long)section);
return [UITableViewCell new];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForConversationAtIndexPath:(NSIndexPath *)indexPath
{
HomeViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:HomeViewCell.cellReuseIdentifier];
OWSAssertDebug(cell);
ThreadViewModel *thread = [self threadViewModelForIndexPath:indexPath];
2018-09-09 21:08:23 +02:00
BOOL isBlocked = [self.blocklistCache isThreadBlocked:thread.threadRecord];
[cell configureWithThread:thread isBlocked:isBlocked];
2018-04-21 17:12:58 +02:00
// TODO: Work with Nancy to confirm that this is accessible via Appium.
NSString *cellName = [NSString stringWithFormat:@"conversation-%@", NSUUID.UUID.UUIDString];
cell.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, cellName);
2014-10-29 21:58:58 +01:00
return cell;
}
2014-10-29 21:58:58 +01:00
- (UITableViewCell *)cellForArchivedConversationsRow:(UITableView *)tableView
{
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:kArchivedConversationsReuseIdentifier];
OWSAssertDebug(cell);
2018-07-13 15:59:33 +02:00
[OWSTableItem configureCell:cell];
for (UIView *subview in cell.contentView.subviews) {
[subview removeFromSuperview];
}
2018-06-29 23:00:22 +02:00
UIImage *disclosureImage = [UIImage imageNamed:(CurrentAppContext().isRTL ? @"NavBarBack" : @"NavBarBackRTL")];
OWSAssertDebug(disclosureImage);
UIImageView *disclosureImageView = [UIImageView new];
disclosureImageView.image = [disclosureImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
disclosureImageView.tintColor = [UIColor colorWithRGBHex:0xd1d1d6];
[disclosureImageView setContentHuggingHigh];
[disclosureImageView setCompressionResistanceHigh];
UILabel *label = [UILabel new];
label.text = NSLocalizedString(@"HOME_VIEW_ARCHIVED_CONVERSATIONS", @"Label for 'archived conversations' button.");
label.textAlignment = NSTextAlignmentCenter;
label.font = [UIFont ows_dynamicTypeBodyFont];
2018-07-13 15:50:49 +02:00
label.textColor = Theme.primaryColor;
UIStackView *stackView = [UIStackView new];
stackView.axis = UILayoutConstraintAxisHorizontal;
stackView.spacing = 5;
// If alignment isn't set, UIStackView uses the height of
// disclosureImageView, even if label has a higher desired height.
stackView.alignment = UIStackViewAlignmentCenter;
2018-04-24 17:42:04 +02:00
[stackView addArrangedSubview:label];
[stackView addArrangedSubview:disclosureImageView];
[cell.contentView addSubview:stackView];
[stackView autoCenterInSuperview];
// Constrain to cell margins.
[stackView autoPinEdgeToSuperviewMargin:ALEdgeLeading relation:NSLayoutRelationGreaterThanOrEqual];
[stackView autoPinEdgeToSuperviewMargin:ALEdgeTrailing relation:NSLayoutRelationGreaterThanOrEqual];
2018-06-20 21:15:33 +02:00
[stackView autoPinEdgeToSuperviewMargin:ALEdgeTop];
[stackView autoPinEdgeToSuperviewMargin:ALEdgeBottom];
cell.accessibilityIdentifier = ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"archived_conversations");
return cell;
}
2017-09-06 19:59:39 +02:00
- (TSThread *)threadForIndexPath:(NSIndexPath *)indexPath
{
__block TSThread *thread = nil;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
2017-09-06 19:59:39 +02:00
thread = [[transaction extension:TSThreadDatabaseViewExtensionName] objectAtIndexPath:indexPath
withMappings:self.threadMappings];
}];
if (![thread isKindOfClass:[TSThread class]]) {
2018-09-19 18:14:15 +02:00
OWSLogError(@"Invalid object in thread view: %@", [thread class]);
[OWSStorage incrementVersionOfDatabaseExtension:TSThreadDatabaseViewExtensionName];
}
return thread;
2014-10-29 21:58:58 +01:00
}
- (void)pullToRefreshPerformed:(UIRefreshControl *)refreshControl
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
OWSLogInfo(@"beggining refreshing.");
[[AppEnvironment.shared.messageFetcherJob run].ensure(^{
OWSLogInfo(@"ending refreshing.");
[refreshControl endRefreshing];
}) retainUntilComplete];
}
2018-12-07 17:37:21 +01:00
#pragma mark - Edit Actions
- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
2017-09-06 19:59:39 +02:00
forRowAtIndexPath:(NSIndexPath *)indexPath
{
return;
}
2018-06-12 17:27:32 +02:00
- (nullable NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath
2017-09-06 19:59:39 +02:00
{
HomeViewControllerSection section = (HomeViewControllerSection)indexPath.section;
switch (section) {
case HomeViewControllerSectionReminders: {
return @[];
}
case HomeViewControllerSectionConversations: {
UITableViewRowAction *deleteAction =
[UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault
title:NSLocalizedString(@"TXT_DELETE_TITLE", nil)
handler:^(UITableViewRowAction *action, NSIndexPath *swipedIndexPath) {
[self tableViewCellTappedDelete:swipedIndexPath];
}];
2019-06-17 02:17:00 +02:00
/**
UITableViewRowAction *archiveAction;
if (self.homeViewMode == HomeViewMode_Inbox) {
archiveAction = [UITableViewRowAction
rowActionWithStyle:UITableViewRowActionStyleNormal
title:NSLocalizedString(@"ARCHIVE_ACTION",
@"Pressing this button moves a thread from the inbox to the archive")
handler:^(UITableViewRowAction *_Nonnull action, NSIndexPath *_Nonnull tappedIndexPath) {
[self archiveIndexPath:tappedIndexPath];
}];
} else {
archiveAction = [UITableViewRowAction
rowActionWithStyle:UITableViewRowActionStyleNormal
title:NSLocalizedString(@"UNARCHIVE_ACTION",
@"Pressing this button moves an archived thread from the archive back to "
@"the inbox")
handler:^(UITableViewRowAction *_Nonnull action, NSIndexPath *_Nonnull tappedIndexPath) {
[self archiveIndexPath:tappedIndexPath];
}];
}
2019-06-17 02:17:00 +02:00
*/
2018-12-07 17:37:21 +01:00
// The first action will be auto-performed for "very long swipes".
return @[
2019-06-17 02:17:00 +02:00
/*archiveAction,*/
2018-12-07 17:37:21 +01:00
deleteAction,
];
}
case HomeViewControllerSectionArchiveButton: {
return @[];
}
}
}
2017-09-06 19:59:39 +02:00
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
HomeViewControllerSection section = (HomeViewControllerSection)indexPath.section;
switch (section) {
case HomeViewControllerSectionReminders: {
return NO;
}
case HomeViewControllerSectionConversations: {
return YES;
}
case HomeViewControllerSectionArchiveButton: {
return NO;
}
2018-05-14 17:47:13 +02:00
}
}
#pragma mark - UISearchBarDelegate
2018-06-11 18:15:46 +02:00
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar
2018-06-11 18:15:46 +02:00
{
[self scrollSearchBarToTopAnimated:NO];
2018-06-11 18:15:46 +02:00
[self updateSearchResultsVisibility];
[self ensureSearchBarCancelButton];
2018-06-11 18:15:46 +02:00
}
- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar
{
2018-06-11 18:15:46 +02:00
[self updateSearchResultsVisibility];
[self ensureSearchBarCancelButton];
2018-06-11 18:15:46 +02:00
}
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
2018-06-11 18:15:46 +02:00
{
[self updateSearchResultsVisibility];
[self ensureSearchBarCancelButton];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
2018-06-11 18:15:46 +02:00
[self updateSearchResultsVisibility];
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
self.searchBar.text = nil;
[self.searchBar resignFirstResponder];
OWSAssertDebug(!self.searchBar.isFirstResponder);
[self updateSearchResultsVisibility];
[self ensureSearchBarCancelButton];
}
- (void)ensureSearchBarCancelButton
{
self.searchBar.showsCancelButton = (self.searchBar.isFirstResponder || self.searchBar.text.length > 0);
}
2018-06-11 18:15:46 +02:00
- (void)updateSearchResultsVisibility
{
OWSAssertIsOnMainThread();
2018-06-11 18:52:46 +02:00
NSString *searchText = self.searchBar.text.ows_stripped;
2018-06-13 17:37:01 +02:00
self.searchResultsController.searchText = searchText;
2018-06-11 18:52:46 +02:00
BOOL isSearching = searchText.length > 0;
2018-06-11 18:15:46 +02:00
self.searchResultsController.view.hidden = !isSearching;
2018-06-13 19:35:55 +02:00
if (isSearching) {
[self scrollSearchBarToTopAnimated:NO];
2018-06-13 19:35:55 +02:00
self.tableView.scrollEnabled = NO;
} else {
self.tableView.scrollEnabled = YES;
}
}
- (void)scrollSearchBarToTopAnimated:(BOOL)isAnimated
{
CGFloat topInset = self.topLayoutGuide.length;
[self.tableView setContentOffset:CGPointMake(0, -topInset) animated:isAnimated];
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
[self.searchBar resignFirstResponder];
OWSAssertDebug(!self.searchBar.isFirstResponder);
}
#pragma mark - ConversationSearchViewDelegate
- (void)conversationSearchViewWillBeginDragging
{
[self.searchBar resignFirstResponder];
OWSAssertDebug(!self.searchBar.isFirstResponder);
}
2014-10-29 21:58:58 +01:00
#pragma mark - HomeFeedTableViewCellDelegate
2017-09-06 19:59:39 +02:00
- (void)tableViewCellTappedDelete:(NSIndexPath *)indexPath
{
if (indexPath.section != HomeViewControllerSectionConversations) {
OWSFailDebug(@"failure: unexpected section: %lu", (unsigned long)indexPath.section);
return;
}
TSThread *thread = [self threadForIndexPath:indexPath];
2018-12-07 17:37:21 +01:00
__weak HomeViewController *weakSelf = self;
UIAlertController *alert =
[UIAlertController alertControllerWithTitle:NSLocalizedString(@"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE",
@"Title for the 'conversation delete confirmation' alert.")
message:NSLocalizedString(@"CONVERSATION_DELETE_CONFIRMATION_ALERT_MESSAGE",
@"Message for the 'conversation delete confirmation' alert.")
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_DELETE_TITLE", nil)
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction *action) {
[weakSelf deleteThread:thread];
}]];
[alert addAction:[OWSAlerts cancelAction]];
[self presentAlert:alert];
}
2017-09-06 19:59:39 +02:00
- (void)deleteThread:(TSThread *)thread
{
2014-12-05 23:38:13 +01:00
[self.editingDbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
/* Loki: Orignal Code
=====================
2018-12-11 23:07:50 +01:00
if ([thread isKindOfClass:[TSGroupThread class]]) {
TSGroupThread *groupThread = (TSGroupThread *)thread;
if (groupThread.isLocalUserInGroup) {
[groupThread softDeleteGroupThreadWithTransaction:transaction];
return;
}
}
*/
2018-12-11 23:07:50 +01:00
// Loki: For now hard delete all groups
2017-09-06 19:59:39 +02:00
[thread removeWithTransaction:transaction];
2014-12-05 23:38:13 +01:00
}];
// Loki: Post notification
[[NSNotificationCenter defaultCenter] postNotificationName:NSNotification.threadDeleted object:nil userInfo:@{ @"threadId": thread.uniqueId }];
[self updateViewState];
2014-10-29 21:58:58 +01:00
}
2017-09-06 19:59:39 +02:00
- (void)archiveIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section != HomeViewControllerSectionConversations) {
OWSFailDebug(@"failure: unexpected section: %lu", (unsigned long)indexPath.section);
return;
}
TSThread *thread = [self threadForIndexPath:indexPath];
2014-12-05 23:38:13 +01:00
[self.editingDbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
switch (self.homeViewMode) {
case HomeViewMode_Inbox:
[thread archiveThreadWithTransaction:transaction];
break;
case HomeViewMode_Archive:
[thread unarchiveThreadWithTransaction:transaction];
break;
}
2014-12-05 23:38:13 +01:00
}];
[self updateViewState];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
OWSLogInfo(@"%ld %ld", (long)indexPath.row, (long)indexPath.section);
[self.searchBar resignFirstResponder];
HomeViewControllerSection section = (HomeViewControllerSection)indexPath.section;
switch (section) {
case HomeViewControllerSectionReminders: {
break;
}
case HomeViewControllerSectionConversations: {
TSThread *thread = [self threadForIndexPath:indexPath];
Faster conversation presentation. There are multiple places in the codebase we present a conversation. We used to have some very conservative machinery around how this was done, for fear of failing to present the call view controller, which would have left a hidden call in the background. We've since addressed that concern more thoroughly via the separate calling UIWindow. As such, the remaining presentation machinery is overly complex and inflexible for what we need. Sometimes we want to animate-push the conversation. (tap on home, tap on "send message" in contact card/group members) Sometimes we want to dismiss a modal, to reveal the conversation behind it (contact picker, group creation) Sometimes we want to present the conversation with no animation (becoming active from a notification) We also want to ensure that we're never pushing more than one conversation view controller, which was previously a problem since we were "pushing" a newly constructed VC in response to these myriad actions. It turned out there were certain code paths that caused multiple actions to be fired in rapid succession which pushed multiple ConversationVC's. The built-in method: `setViewControllers:animated` easily ensures we only have one ConversationVC on the stack, while being composable enough to faciliate the various more efficient animations we desire. The only thing lost with the complex methods is that the naive `presentViewController:` can fail, e.g. if another view is already presented. E.g. if an alert appears *just* before the user taps compose, the contact picker will fail to present. Since we no longer depend on this for presenting the CallViewController, this isn't catostrophic, and in fact, arguable preferable, since we want the user to read and dismiss any alert explicitly. // FREEBIE
2018-08-18 22:54:35 +02:00
[self presentThread:thread action:ConversationViewActionNone animated:YES];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
break;
}
case HomeViewControllerSectionArchiveButton: {
[self showArchivedConversations];
break;
}
}
}
Faster conversation presentation. There are multiple places in the codebase we present a conversation. We used to have some very conservative machinery around how this was done, for fear of failing to present the call view controller, which would have left a hidden call in the background. We've since addressed that concern more thoroughly via the separate calling UIWindow. As such, the remaining presentation machinery is overly complex and inflexible for what we need. Sometimes we want to animate-push the conversation. (tap on home, tap on "send message" in contact card/group members) Sometimes we want to dismiss a modal, to reveal the conversation behind it (contact picker, group creation) Sometimes we want to present the conversation with no animation (becoming active from a notification) We also want to ensure that we're never pushing more than one conversation view controller, which was previously a problem since we were "pushing" a newly constructed VC in response to these myriad actions. It turned out there were certain code paths that caused multiple actions to be fired in rapid succession which pushed multiple ConversationVC's. The built-in method: `setViewControllers:animated` easily ensures we only have one ConversationVC on the stack, while being composable enough to faciliate the various more efficient animations we desire. The only thing lost with the complex methods is that the naive `presentViewController:` can fail, e.g. if another view is already presented. E.g. if an alert appears *just* before the user taps compose, the contact picker will fail to present. Since we no longer depend on this for presenting the CallViewController, this isn't catostrophic, and in fact, arguable preferable, since we want the user to read and dismiss any alert explicitly. // FREEBIE
2018-08-18 22:54:35 +02:00
- (void)presentThread:(TSThread *)thread action:(ConversationViewAction)action animated:(BOOL)isAnimated
2018-06-11 21:31:54 +02:00
{
Faster conversation presentation. There are multiple places in the codebase we present a conversation. We used to have some very conservative machinery around how this was done, for fear of failing to present the call view controller, which would have left a hidden call in the background. We've since addressed that concern more thoroughly via the separate calling UIWindow. As such, the remaining presentation machinery is overly complex and inflexible for what we need. Sometimes we want to animate-push the conversation. (tap on home, tap on "send message" in contact card/group members) Sometimes we want to dismiss a modal, to reveal the conversation behind it (contact picker, group creation) Sometimes we want to present the conversation with no animation (becoming active from a notification) We also want to ensure that we're never pushing more than one conversation view controller, which was previously a problem since we were "pushing" a newly constructed VC in response to these myriad actions. It turned out there were certain code paths that caused multiple actions to be fired in rapid succession which pushed multiple ConversationVC's. The built-in method: `setViewControllers:animated` easily ensures we only have one ConversationVC on the stack, while being composable enough to faciliate the various more efficient animations we desire. The only thing lost with the complex methods is that the naive `presentViewController:` can fail, e.g. if another view is already presented. E.g. if an alert appears *just* before the user taps compose, the contact picker will fail to present. Since we no longer depend on this for presenting the CallViewController, this isn't catostrophic, and in fact, arguable preferable, since we want the user to read and dismiss any alert explicitly. // FREEBIE
2018-08-18 22:54:35 +02:00
[self presentThread:thread action:action focusMessageId:nil animated:isAnimated];
2018-06-11 21:31:54 +02:00
}
- (void)presentThread:(TSThread *)thread
action:(ConversationViewAction)action
focusMessageId:(nullable NSString *)focusMessageId
Faster conversation presentation. There are multiple places in the codebase we present a conversation. We used to have some very conservative machinery around how this was done, for fear of failing to present the call view controller, which would have left a hidden call in the background. We've since addressed that concern more thoroughly via the separate calling UIWindow. As such, the remaining presentation machinery is overly complex and inflexible for what we need. Sometimes we want to animate-push the conversation. (tap on home, tap on "send message" in contact card/group members) Sometimes we want to dismiss a modal, to reveal the conversation behind it (contact picker, group creation) Sometimes we want to present the conversation with no animation (becoming active from a notification) We also want to ensure that we're never pushing more than one conversation view controller, which was previously a problem since we were "pushing" a newly constructed VC in response to these myriad actions. It turned out there were certain code paths that caused multiple actions to be fired in rapid succession which pushed multiple ConversationVC's. The built-in method: `setViewControllers:animated` easily ensures we only have one ConversationVC on the stack, while being composable enough to faciliate the various more efficient animations we desire. The only thing lost with the complex methods is that the naive `presentViewController:` can fail, e.g. if another view is already presented. E.g. if an alert appears *just* before the user taps compose, the contact picker will fail to present. Since we no longer depend on this for presenting the CallViewController, this isn't catostrophic, and in fact, arguable preferable, since we want the user to read and dismiss any alert explicitly. // FREEBIE
2018-08-18 22:54:35 +02:00
animated:(BOOL)isAnimated
2017-04-18 22:08:01 +02:00
{
if (thread == nil) {
2018-08-27 16:29:51 +02:00
OWSFailDebug(@"Thread unexpectedly nil");
return;
}
2017-08-03 19:16:45 +02:00
DispatchMainThreadSafe(^{
Faster conversation presentation. There are multiple places in the codebase we present a conversation. We used to have some very conservative machinery around how this was done, for fear of failing to present the call view controller, which would have left a hidden call in the background. We've since addressed that concern more thoroughly via the separate calling UIWindow. As such, the remaining presentation machinery is overly complex and inflexible for what we need. Sometimes we want to animate-push the conversation. (tap on home, tap on "send message" in contact card/group members) Sometimes we want to dismiss a modal, to reveal the conversation behind it (contact picker, group creation) Sometimes we want to present the conversation with no animation (becoming active from a notification) We also want to ensure that we're never pushing more than one conversation view controller, which was previously a problem since we were "pushing" a newly constructed VC in response to these myriad actions. It turned out there were certain code paths that caused multiple actions to be fired in rapid succession which pushed multiple ConversationVC's. The built-in method: `setViewControllers:animated` easily ensures we only have one ConversationVC on the stack, while being composable enough to faciliate the various more efficient animations we desire. The only thing lost with the complex methods is that the naive `presentViewController:` can fail, e.g. if another view is already presented. E.g. if an alert appears *just* before the user taps compose, the contact picker will fail to present. Since we no longer depend on this for presenting the CallViewController, this isn't catostrophic, and in fact, arguable preferable, since we want the user to read and dismiss any alert explicitly. // FREEBIE
2018-08-18 22:54:35 +02:00
ConversationViewController *conversationVC = [ConversationViewController new];
[conversationVC configureForThread:thread action:action focusMessageId:focusMessageId];
self.lastThread = thread;
if (self.homeViewMode == HomeViewMode_Archive) {
[self.navigationController pushViewController:conversationVC animated:isAnimated];
} else {
[self.navigationController setViewControllers:@[ self, conversationVC ] animated:isAnimated];
}
});
2014-10-29 21:58:58 +01:00
}
#pragma mark - Groupings
2014-10-29 21:58:58 +01:00
- (YapDatabaseViewMappings *)threadMappings
{
OWSAssertDebug(_threadMappings != nil);
return _threadMappings;
}
- (void)showInboxGrouping
{
OWSAssertDebug(self.homeViewMode == HomeViewMode_Archive);
[self.navigationController popToRootViewControllerAnimated:YES];
}
- (void)showArchivedConversations
{
OWSAssertDebug(self.homeViewMode == HomeViewMode_Inbox);
// When showing archived conversations, we want to use a conventional "back" button
// to return to the "inbox" home view.
[self applyArchiveBackButton];
// Push a separate instance of this view using "archive" mode.
HomeViewController *homeView = [HomeViewController new];
homeView.homeViewMode = HomeViewMode_Archive;
[self.navigationController pushViewController:homeView animated:YES];
}
- (NSString *)currentGrouping
{
switch (self.homeViewMode) {
case HomeViewMode_Inbox:
return TSInboxGroup;
case HomeViewMode_Archive:
return TSArchiveGroup;
}
}
- (void)updateMappings
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
self.threadMappings = [[YapDatabaseViewMappings alloc]
initWithGroups:@[ kReminderViewPseudoGroup, self.currentGrouping, kArchiveButtonPseudoGroup ]
view:TSThreadDatabaseViewExtensionName];
[self.threadMappings setIsReversed:YES forGroup:self.currentGrouping];
2017-07-26 18:39:43 +02:00
[self resetMappings];
2018-04-21 21:22:40 +02:00
[self reloadTableViewData];
[self updateViewState];
[self updateReminderViews];
2014-10-29 21:58:58 +01:00
}
2019-02-21 15:53:49 +01:00
#pragma mark - Database delegates
2017-09-06 19:59:39 +02:00
- (YapDatabaseConnection *)uiDatabaseConnection
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
if (!_uiDatabaseConnection) {
_uiDatabaseConnection = [OWSPrimaryStorage.sharedManager newDatabaseConnection];
// default is 250
_uiDatabaseConnection.objectCacheLimit = 500;
[_uiDatabaseConnection beginLongLivedReadTransaction];
}
return _uiDatabaseConnection;
}
- (void)yapDatabaseModifiedExternally:(NSNotification *)notification
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
OWSLogVerbose(@"");
if (self.shouldObserveDBModifications) {
// External database modifications can't be converted into incremental updates,
// so rebuild everything. This is expensive and usually isn't necessary, but
// there's no alternative.
2018-02-22 18:07:11 +01:00
//
// We don't need to do this if we're not observing db modifications since we'll
// do it when we resume.
[self resetMappings];
}
}
2017-09-06 19:59:39 +02:00
- (void)yapDatabaseModified:(NSNotification *)notification
{
2017-12-19 17:38:25 +01:00
OWSAssertIsOnMainThread();
if (!self.shouldObserveDBModifications) {
return;
}
2017-09-06 19:59:39 +02:00
NSArray *notifications = [self.uiDatabaseConnection beginLongLivedReadTransaction];
if (![[self.uiDatabaseConnection ext:TSThreadDatabaseViewExtensionName] hasChangesForGroup:self.currentGrouping
inNotifications:notifications]) {
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
2018-04-23 20:13:55 +02:00
[self.threadMappings updateWithTransaction:transaction];
}];
[self updateViewState];
return;
}
// If the user hasn't already granted contact access
// we don't want to request until they receive a message.
2018-04-21 17:12:58 +02:00
__block BOOL hasAnyMessages;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
hasAnyMessages = [self hasAnyMessagesWithTransaction:transaction];
}];
if (hasAnyMessages) {
[self.contactsManager requestSystemContactsOnce];
}
NSArray *sectionChanges = nil;
NSArray *rowChanges = nil;
[[self.uiDatabaseConnection ext:TSThreadDatabaseViewExtensionName] getSectionChanges:&sectionChanges
rowChanges:&rowChanges
forNotifications:notifications
withMappings:self.threadMappings];
// We want this regardless of if we're currently viewing the archive.
// So we run it before the early return
[self updateViewState];
if ([sectionChanges count] == 0 && [rowChanges count] == 0) {
return;
}
if ([self updateHasArchivedThreadsRow]) {
[self.tableView reloadData];
return;
}
[self.tableView beginUpdates];
for (YapDatabaseViewSectionChange *sectionChange in sectionChanges) {
switch (sectionChange.type) {
case YapDatabaseViewChangeDelete: {
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionChange.index]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
case YapDatabaseViewChangeInsert: {
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionChange.index]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
case YapDatabaseViewChangeUpdate:
case YapDatabaseViewChangeMove:
break;
}
}
for (YapDatabaseViewRowChange *rowChange in rowChanges) {
NSString *key = rowChange.collectionKey.key;
OWSAssertDebug(key);
[self.threadViewModelCache removeObjectForKey:key];
switch (rowChange.type) {
case YapDatabaseViewChangeDelete: {
[self.tableView deleteRowsAtIndexPaths:@[ rowChange.indexPath ]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
case YapDatabaseViewChangeInsert: {
[self.tableView insertRowsAtIndexPaths:@[ rowChange.newIndexPath ]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
case YapDatabaseViewChangeMove: {
[self.tableView deleteRowsAtIndexPaths:@[ rowChange.indexPath ]
withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView insertRowsAtIndexPaths:@[ rowChange.newIndexPath ]
withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
case YapDatabaseViewChangeUpdate: {
[self.tableView reloadRowsAtIndexPaths:@[ rowChange.indexPath ]
withRowAnimation:UITableViewRowAnimationNone];
break;
}
}
}
[self.tableView endUpdates];
}
2014-11-25 19:06:09 +01:00
- (NSUInteger)numberOfThreadsInGroup:(NSString *)group
2017-09-06 19:59:39 +02:00
{
// We need to consult the db view, not the mapping since the mapping only knows about
// the current group.
__block NSUInteger result;
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
YapDatabaseViewTransaction *viewTransaction = [transaction ext:TSThreadDatabaseViewExtensionName];
result = [viewTransaction numberOfItemsInGroup:group];
}];
return result;
}
- (NSUInteger)numberOfInboxThreads
{
return [self numberOfThreadsInGroup:TSInboxGroup];
}
- (NSUInteger)numberOfArchivedThreads
{
return [self numberOfThreadsInGroup:TSArchiveGroup];
}
- (void)updateViewState
{
2019-02-21 15:53:49 +01:00
if (self.shouldShowFirstConversationCue) {
[_tableView setHidden:YES];
2019-02-18 17:44:27 +01:00
[self.emptyInboxView setHidden:NO];
[self.firstConversationCueView setHidden:NO];
2019-02-19 15:52:10 +01:00
[self updateFirstConversationLabel];
} else {
[_tableView setHidden:NO];
2019-02-18 17:44:27 +01:00
[self.emptyInboxView setHidden:YES];
[self.firstConversationCueView setHidden:YES];
2014-11-25 19:06:09 +01:00
}
}
2019-02-21 15:53:49 +01:00
- (BOOL)shouldShowFirstConversationCue
{
return (self.homeViewMode == HomeViewMode_Inbox && self.numberOfInboxThreads == 0
&& self.numberOfArchivedThreads == 0 && !AppPreferences.hasDimissedFirstConversationCue
&& !SSKPreferences.hasSavedThread);
}
2018-08-07 23:36:34 +02:00
// We want to delay asking for a review until an opportune time.
// If the user has *just* launched Signal they intend to do something, we don't want to interrupt them.
// If the user hasn't sent a message, we don't want to ask them for a review yet.
- (void)requestReviewIfAppropriate
{
2018-08-31 19:44:13 +02:00
if (self.hasEverAppeared && Environment.shared.preferences.hasSentAMessage) {
OWSLogDebug(@"requesting review");
2018-08-07 23:36:34 +02:00
if (@available(iOS 10, *)) {
// In Debug this pops up *every* time, which is helpful, but annoying.
// In Production this will pop up at most 3 times per 365 days.
#ifndef DEBUG
static dispatch_once_t onceToken;
// Despite `SKStoreReviewController` docs, some people have reported seeing the "request review" prompt
// repeatedly after first installation. Let's make sure it only happens at most once per launch.
dispatch_once(&onceToken, ^{
[SKStoreReviewController requestReview];
});
2018-08-07 23:36:34 +02:00
#endif
}
} else {
OWSLogDebug(@"not requesting review");
2018-08-07 23:36:34 +02:00
}
}
#pragma mark - OWSBlockListCacheDelegate
- (void)blockListCacheDidUpdate:(OWSBlockListCache *_Nonnull)blocklistCache
{
2018-09-15 16:17:08 +02:00
OWSLogVerbose(@"");
[self reloadTableViewData];
}
2014-10-29 21:58:58 +01:00
@end
2018-06-12 17:27:32 +02:00
NS_ASSUME_NONNULL_END