mirror of https://github.com/oxen-io/session-ios
Merge branch 'dev' into feature/session-id-blinding-part-2
# Conflicts: # Session.xcodeproj/project.pbxproj # Session/Conversations/Views & Modals/JoinOpenGroupModal.swift # Session/Open Groups/JoinOpenGroupVC.swift # Session/Settings/SettingsVC.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift # SessionMessagingKit/Sending & Receiving/MessageReceiver.swiftpull/592/head
commit
c3b855646f
@ -1,49 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class OWSPrimaryStorage;
|
||||
@class TSGroupModel;
|
||||
@class TSThread;
|
||||
|
||||
extern NSString *const kNSNotificationName_BlockListDidChange;
|
||||
|
||||
extern NSString *const kOWSBlockingManager_BlockListCollection;
|
||||
|
||||
// This class can be safely accessed and used from any thread.
|
||||
@interface OWSBlockingManager : NSObject
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
+ (instancetype)sharedManager;
|
||||
|
||||
- (void)addBlockedPhoneNumber:(NSString *)phoneNumber;
|
||||
|
||||
- (void)removeBlockedPhoneNumber:(NSString *)phoneNumber;
|
||||
|
||||
// When updating the block list from a sync message, we don't
|
||||
// want to fire a sync message.
|
||||
- (void)setBlockedPhoneNumbers:(NSArray<NSString *> *)blockedPhoneNumbers sendSyncMessage:(BOOL)sendSyncMessage;
|
||||
|
||||
// TODO convert to property
|
||||
- (NSArray<NSString *> *)blockedPhoneNumbers;
|
||||
|
||||
@property (readonly) NSArray<NSData *> *blockedGroupIds;
|
||||
@property (readonly) NSArray<TSGroupModel *> *blockedGroups;
|
||||
|
||||
- (void)addBlockedGroup:(TSGroupModel *)group;
|
||||
- (void)removeBlockedGroupId:(NSData *)groupId;
|
||||
- (nullable TSGroupModel *)cachedGroupDetailsWithGroupId:(NSData *)groupId;
|
||||
|
||||
- (BOOL)isRecipientIdBlocked:(NSString *)recipientId;
|
||||
- (BOOL)isGroupIdBlocked:(NSData *)groupId;
|
||||
- (BOOL)isThreadBlocked:(TSThread *)thread;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,348 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBlockingManager.h"
|
||||
#import "AppContext.h"
|
||||
#import "AppReadiness.h"
|
||||
#import "NSNotificationCenter+OWS.h"
|
||||
#import "OWSPrimaryStorage.h"
|
||||
#import "SSKEnvironment.h"
|
||||
#import "TSContactThread.h"
|
||||
#import "TSGroupThread.h"
|
||||
#import "YapDatabaseConnection+OWS.h"
|
||||
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
|
||||
#import <SessionUtilitiesKit/SessionUtilitiesKit-Swift.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSString *const kNSNotificationName_BlockListDidChange = @"kNSNotificationName_BlockListDidChange";
|
||||
|
||||
NSString *const kOWSBlockingManager_BlockListCollection = @"kOWSBlockingManager_BlockedPhoneNumbersCollection";
|
||||
|
||||
// These keys are used to persist the current local "block list" state.
|
||||
NSString *const kOWSBlockingManager_BlockedPhoneNumbersKey = @"kOWSBlockingManager_BlockedPhoneNumbersKey";
|
||||
NSString *const kOWSBlockingManager_BlockedGroupMapKey = @"kOWSBlockingManager_BlockedGroupMapKey";
|
||||
|
||||
// These keys are used to persist the most recently synced remote "block list" state.
|
||||
NSString *const kOWSBlockingManager_SyncedBlockedPhoneNumbersKey = @"kOWSBlockingManager_SyncedBlockedPhoneNumbersKey";
|
||||
NSString *const kOWSBlockingManager_SyncedBlockedGroupIdsKey = @"kOWSBlockingManager_SyncedBlockedGroupIdsKey";
|
||||
|
||||
@interface OWSBlockingManager ()
|
||||
|
||||
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
|
||||
|
||||
// We don't store the phone numbers as instances of PhoneNumber to avoid
|
||||
// consistency issues between clients, but these should all be valid e164
|
||||
// phone numbers.
|
||||
@property (atomic, readonly) NSMutableSet<NSString *> *blockedPhoneNumberSet;
|
||||
@property (atomic, readonly) NSMutableDictionary<NSData *, TSGroupModel *> *blockedGroupMap;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSBlockingManager
|
||||
|
||||
+ (instancetype)sharedManager
|
||||
{
|
||||
return SSKEnvironment.shared.blockingManager;
|
||||
}
|
||||
|
||||
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_dbConnection = primaryStorage.newDatabaseConnection;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)observeNotifications
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(applicationDidBecomeActive:)
|
||||
name:OWSApplicationDidBecomeActiveNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (BOOL)isThreadBlocked:(TSThread *)thread
|
||||
{
|
||||
if ([thread isKindOfClass:[TSContactThread class]]) {
|
||||
TSContactThread *contactThread = (TSContactThread *)thread;
|
||||
return [self isRecipientIdBlocked:contactThread.contactSessionID];
|
||||
} else if ([thread isKindOfClass:[TSGroupThread class]]) {
|
||||
TSGroupThread *groupThread = (TSGroupThread *)thread;
|
||||
return [self isGroupIdBlocked:groupThread.groupModel.groupId];
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Contact Blocking
|
||||
|
||||
- (void)addBlockedPhoneNumber:(NSString *)phoneNumber
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
[self ensureLazyInitialization];
|
||||
|
||||
if ([_blockedPhoneNumberSet containsObject:phoneNumber]) {
|
||||
// Ignore redundant changes.
|
||||
return;
|
||||
}
|
||||
|
||||
[_blockedPhoneNumberSet addObject:phoneNumber];
|
||||
}
|
||||
|
||||
[self handleUpdate];
|
||||
}
|
||||
|
||||
- (void)removeBlockedPhoneNumber:(NSString *)phoneNumber
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
[self ensureLazyInitialization];
|
||||
|
||||
if (![_blockedPhoneNumberSet containsObject:phoneNumber]) {
|
||||
// Ignore redundant changes.
|
||||
return;
|
||||
}
|
||||
|
||||
[_blockedPhoneNumberSet removeObject:phoneNumber];
|
||||
}
|
||||
|
||||
[self handleUpdate];
|
||||
}
|
||||
|
||||
- (void)setBlockedPhoneNumbers:(NSArray<NSString *> *)blockedPhoneNumbers sendSyncMessage:(BOOL)sendSyncMessage
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
[self ensureLazyInitialization];
|
||||
|
||||
NSSet *newSet = [NSSet setWithArray:blockedPhoneNumbers];
|
||||
if ([_blockedPhoneNumberSet isEqualToSet:newSet]) {
|
||||
return;
|
||||
}
|
||||
|
||||
_blockedPhoneNumberSet = [newSet mutableCopy];
|
||||
}
|
||||
|
||||
[self handleUpdate:sendSyncMessage];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)blockedPhoneNumbers
|
||||
{
|
||||
@synchronized(self)
|
||||
{
|
||||
[self ensureLazyInitialization];
|
||||
|
||||
return [_blockedPhoneNumberSet.allObjects sortedArrayUsingSelector:@selector(compare:)];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isRecipientIdBlocked:(NSString *)recipientId
|
||||
{
|
||||
return [self.blockedPhoneNumbers containsObject:recipientId];
|
||||
}
|
||||
|
||||
#pragma mark - Group Blocking
|
||||
|
||||
- (NSArray<NSData *> *)blockedGroupIds
|
||||
{
|
||||
@synchronized(self) {
|
||||
[self ensureLazyInitialization];
|
||||
return self.blockedGroupMap.allKeys;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<TSGroupModel *> *)blockedGroups
|
||||
{
|
||||
@synchronized(self) {
|
||||
[self ensureLazyInitialization];
|
||||
return self.blockedGroupMap.allValues;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isGroupIdBlocked:(NSData *)groupId
|
||||
{
|
||||
return self.blockedGroupMap[groupId] != nil;
|
||||
}
|
||||
|
||||
- (nullable TSGroupModel *)cachedGroupDetailsWithGroupId:(NSData *)groupId
|
||||
{
|
||||
@synchronized(self) {
|
||||
return self.blockedGroupMap[groupId];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addBlockedGroup:(TSGroupModel *)groupModel
|
||||
{
|
||||
NSData *groupId = groupModel.groupId;
|
||||
|
||||
@synchronized(self) {
|
||||
[self ensureLazyInitialization];
|
||||
|
||||
if ([self isGroupIdBlocked:groupId]) {
|
||||
// Ignore redundant changes.
|
||||
return;
|
||||
}
|
||||
self.blockedGroupMap[groupId] = groupModel;
|
||||
}
|
||||
|
||||
[self handleUpdate];
|
||||
}
|
||||
|
||||
- (void)removeBlockedGroupId:(NSData *)groupId
|
||||
{
|
||||
@synchronized(self) {
|
||||
[self ensureLazyInitialization];
|
||||
|
||||
if (![self isGroupIdBlocked:groupId]) {
|
||||
// Ignore redundant changes.
|
||||
return;
|
||||
}
|
||||
|
||||
[self.blockedGroupMap removeObjectForKey:groupId];
|
||||
}
|
||||
|
||||
[self handleUpdate];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Updates
|
||||
|
||||
// This should be called every time the block list changes.
|
||||
|
||||
- (void)handleUpdate
|
||||
{
|
||||
// By default, always send a sync message when the block list changes.
|
||||
[self handleUpdate:YES];
|
||||
}
|
||||
|
||||
// TODO label the `sendSyncMessage` param
|
||||
- (void)handleUpdate:(BOOL)sendSyncMessage
|
||||
{
|
||||
NSArray<NSString *> *blockedPhoneNumbers = [self blockedPhoneNumbers];
|
||||
|
||||
[self.dbConnection setObject:blockedPhoneNumbers
|
||||
forKey:kOWSBlockingManager_BlockedPhoneNumbersKey
|
||||
inCollection:kOWSBlockingManager_BlockListCollection];
|
||||
|
||||
NSDictionary *blockedGroupMap;
|
||||
@synchronized(self) {
|
||||
blockedGroupMap = [self.blockedGroupMap copy];
|
||||
}
|
||||
NSArray<NSData *> *blockedGroupIds = blockedGroupMap.allKeys;
|
||||
|
||||
[self.dbConnection setObject:blockedGroupMap
|
||||
forKey:kOWSBlockingManager_BlockedGroupMapKey
|
||||
inCollection:kOWSBlockingManager_BlockListCollection];
|
||||
|
||||
// Update the contact blocked state (so sync'ing won't be busted)
|
||||
NSMutableArray<SNContact *> *contactsToUpdate = [[NSMutableArray alloc] init];
|
||||
|
||||
[[[LKStorage shared] getAllContacts] enumerateObjectsUsingBlock:^(SNContact * _Nonnull obj, BOOL * _Nonnull stop) {
|
||||
// If the blocked flag doesn't match then add it to the array to be saved
|
||||
BOOL contactInBlockedList = [blockedPhoneNumbers containsObject:obj.sessionID];
|
||||
|
||||
if (obj.isBlocked != contactInBlockedList) {
|
||||
obj.isBlocked = contactInBlockedList;
|
||||
[contactsToUpdate addObject:obj];
|
||||
}
|
||||
}];
|
||||
|
||||
if ([contactsToUpdate count] > 0) {
|
||||
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction * _Nonnull transaction) {
|
||||
[contactsToUpdate enumerateObjectsUsingBlock:^(SNContact * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
[[LKStorage shared] setContact:obj usingTransaction:transaction];
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
if (sendSyncMessage) {
|
||||
|
||||
} else {
|
||||
// If this update came from an incoming block list sync message,
|
||||
// update the "synced blocked list" state immediately,
|
||||
// since we're now in sync.
|
||||
//
|
||||
// There could be data loss if both clients modify the block list
|
||||
// at the same time, but:
|
||||
//
|
||||
// a) Block list changes will be rare.
|
||||
// b) Conflicting block list changes will be even rarer.
|
||||
// c) It's unlikely a user will make conflicting changes on two
|
||||
// devices around the same time.
|
||||
// d) There isn't a good way to avoid this.
|
||||
[self saveSyncedBlockListWithPhoneNumbers:blockedPhoneNumbers groupIds:blockedGroupIds];
|
||||
}
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationNameAsync:kNSNotificationName_BlockListDidChange
|
||||
object:nil
|
||||
userInfo:nil];
|
||||
});
|
||||
}
|
||||
|
||||
// This method should only be called from within a synchronized block.
|
||||
- (void)ensureLazyInitialization
|
||||
{
|
||||
if (_blockedPhoneNumberSet) {
|
||||
|
||||
// already loaded
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray<NSString *> *blockedPhoneNumbers =
|
||||
[self.dbConnection objectForKey:kOWSBlockingManager_BlockedPhoneNumbersKey
|
||||
inCollection:kOWSBlockingManager_BlockListCollection];
|
||||
_blockedPhoneNumberSet = [[NSMutableSet alloc] initWithArray:(blockedPhoneNumbers ?: [NSArray new])];
|
||||
|
||||
NSDictionary<NSData *, TSGroupModel *> *storedBlockedGroupMap =
|
||||
[self.dbConnection objectForKey:kOWSBlockingManager_BlockedGroupMapKey
|
||||
inCollection:kOWSBlockingManager_BlockListCollection];
|
||||
if ([storedBlockedGroupMap isKindOfClass:[NSDictionary class]]) {
|
||||
_blockedGroupMap = [storedBlockedGroupMap mutableCopy];
|
||||
} else {
|
||||
_blockedGroupMap = [NSMutableDictionary new];
|
||||
}
|
||||
|
||||
[self observeNotifications];
|
||||
}
|
||||
|
||||
/// Records the last block list which we successfully synced.
|
||||
- (void)saveSyncedBlockListWithPhoneNumbers:(NSArray<NSString *> *)blockedPhoneNumbers
|
||||
groupIds:(NSArray<NSData *> *)blockedGroupIds
|
||||
{
|
||||
[self.dbConnection setObject:blockedPhoneNumbers
|
||||
forKey:kOWSBlockingManager_SyncedBlockedPhoneNumbersKey
|
||||
inCollection:kOWSBlockingManager_BlockListCollection];
|
||||
|
||||
[self.dbConnection setObject:blockedGroupIds
|
||||
forKey:kOWSBlockingManager_SyncedBlockedGroupIdsKey
|
||||
inCollection:kOWSBlockingManager_BlockListCollection];
|
||||
}
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)notification
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,47 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc(SNBlockingManagerRemovalMigration)
|
||||
public class BlockingManagerRemovalMigration: OWSDatabaseMigration {
|
||||
@objc
|
||||
class func migrationId() -> String {
|
||||
return "004"
|
||||
}
|
||||
|
||||
override public func runUp(completion: @escaping OWSDatabaseMigrationCompletion) {
|
||||
self.doMigrationAsync(completion: completion)
|
||||
}
|
||||
|
||||
private func doMigrationAsync(completion: @escaping OWSDatabaseMigrationCompletion) {
|
||||
// These are the legacy keys that were used to persist the "block list" state
|
||||
let kOWSBlockingManager_BlockListCollection: String = "kOWSBlockingManager_BlockedPhoneNumbersCollection"
|
||||
let kOWSBlockingManager_BlockedPhoneNumbersKey: String = "kOWSBlockingManager_BlockedPhoneNumbersKey"
|
||||
|
||||
let dbConnection: YapDatabaseConnection = primaryStorage.newDatabaseConnection()
|
||||
|
||||
let blockedSessionIds: Set<String> = Set(dbConnection.object(
|
||||
forKey: kOWSBlockingManager_BlockedPhoneNumbersKey,
|
||||
inCollection: kOWSBlockingManager_BlockListCollection
|
||||
) as? [String] ?? [])
|
||||
|
||||
Storage.write(
|
||||
with: { transaction in
|
||||
Storage.shared.getAllContacts(with: transaction)
|
||||
.filter { contact -> Bool in blockedSessionIds.contains(contact.sessionID) }
|
||||
.forEach { contact in
|
||||
contact.isBlocked = true
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
}
|
||||
|
||||
// Now that the values have been migrated we can clear out the old collection
|
||||
transaction.removeAllObjects(inCollection: kOWSBlockingManager_BlockListCollection)
|
||||
|
||||
self.save(with: transaction) // Intentionally capture self
|
||||
},
|
||||
completion: {
|
||||
completion(true, true)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc(OWSBlockListCacheDelegate)
|
||||
public protocol BlockListCacheDelegate: class {
|
||||
func blockListCacheDidUpdate(_ blocklistCache: BlockListCache)
|
||||
}
|
||||
|
||||
/// A performant cache for which contacts/groups are blocked.
|
||||
///
|
||||
/// The source of truth for which contacts and groups are blocked is the `blockingManager`, but because
|
||||
/// those accessors are made to be thread safe, they can be slow in tight loops, e.g. when rendering table
|
||||
/// view cells.
|
||||
///
|
||||
/// Typically you'll want to create a Cache, update it to the latest state while simultaneously being informed
|
||||
/// of any future changes to block list state.
|
||||
///
|
||||
/// class SomeViewController: BlockListCacheDelegate {
|
||||
/// let blockListCache = BlockListCache()
|
||||
/// func viewDidLoad() {
|
||||
/// super.viewDidLoad()
|
||||
/// blockListCache.startObservingAndSyncState(delegate: self)
|
||||
/// self.updateAnyViewsWhichDepondOnBlockListCache()
|
||||
/// }
|
||||
///
|
||||
/// func blockListCacheDidUpdate(_ blocklistCache: BlockListCache) {
|
||||
/// self.updateAnyViewsWhichDepondOnBlockListCache()
|
||||
/// }
|
||||
///
|
||||
/// ...
|
||||
/// }
|
||||
///
|
||||
@objc(OWSBlockListCache)
|
||||
public class BlockListCache: NSObject {
|
||||
|
||||
private var blockedRecipientIds: Set<String> = Set()
|
||||
private var blockedGroupIds: Set<Data> = Set()
|
||||
private let serialQueue: DispatchQueue = DispatchQueue(label: "BlockListCache")
|
||||
weak var delegate: BlockListCacheDelegate?
|
||||
|
||||
private var blockingManager: OWSBlockingManager {
|
||||
return OWSBlockingManager.shared()
|
||||
}
|
||||
|
||||
/// Generally something which wants to use this cache wants to do 3 things
|
||||
/// 1. get the cache on the latest state
|
||||
/// 2. update the cache whenever the blockingManager's state changes
|
||||
/// 3. be notified when the cache updates
|
||||
/// This method does all three.
|
||||
@objc
|
||||
public func startObservingAndSyncState(delegate: BlockListCacheDelegate) {
|
||||
self.delegate = delegate
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(blockListDidChange),
|
||||
name: NSNotification.Name(rawValue: kNSNotificationName_BlockListDidChange),
|
||||
object: nil)
|
||||
updateWithoutNotifyingDelegate()
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@objc
|
||||
func blockListDidChange() {
|
||||
self.update()
|
||||
}
|
||||
|
||||
@objc(isRecipientIdBlocked:)
|
||||
public func isBlocked(recipientId: String) -> Bool {
|
||||
return serialQueue.sync {
|
||||
blockedRecipientIds.contains(recipientId)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(isGroupIdBlocked:)
|
||||
public func isBlocked(groupId: Data) -> Bool {
|
||||
return serialQueue.sync {
|
||||
blockedGroupIds.contains(groupId)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(isThreadBlocked:)
|
||||
public func isBlocked(thread: TSThread) -> Bool {
|
||||
switch thread {
|
||||
case let contactThread as TSContactThread:
|
||||
return serialQueue.sync {
|
||||
blockedRecipientIds.contains(contactThread.contactSessionID())
|
||||
}
|
||||
case let groupThread as TSGroupThread:
|
||||
return serialQueue.sync {
|
||||
blockedGroupIds.contains(groupThread.groupModel.groupId)
|
||||
}
|
||||
default:
|
||||
owsFailDebug("\(self.logTag) in \(#function) unexpected thread type: \(type(of: thread))")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
public func update() {
|
||||
updateWithoutNotifyingDelegate()
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.blockListCacheDidUpdate(self)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateWithoutNotifyingDelegate() {
|
||||
let blockedRecipientIds = Set(blockingManager.blockedPhoneNumbers())
|
||||
let blockedGroupIds = Set(blockingManager.blockedGroupIds)
|
||||
update(blockedRecipientIds: blockedRecipientIds, blockedGroupIds: blockedGroupIds)
|
||||
}
|
||||
|
||||
private func update(blockedRecipientIds: Set<String>, blockedGroupIds: Set<Data>) {
|
||||
serialQueue.sync {
|
||||
self.blockedRecipientIds = blockedRecipientIds
|
||||
self.blockedGroupIds = blockedGroupIds
|
||||
}
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class Contact;
|
||||
@class OWSBlockingManager;
|
||||
@class SignalAccount;
|
||||
@class TSGroupModel;
|
||||
@class TSThread;
|
||||
|
||||
typedef void (^BlockActionCompletionBlock)(BOOL isBlocked);
|
||||
|
||||
@interface BlockListUIUtils : NSObject
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
#pragma mark - Block
|
||||
|
||||
+ (void)showBlockThreadActionSheet:(TSThread *)thread
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock;
|
||||
|
||||
+ (void)showBlockPhoneNumberActionSheet:(NSString *)phoneNumber
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock;
|
||||
|
||||
+ (void)showBlockSignalAccountActionSheet:(SignalAccount *)signalAccount
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock;
|
||||
|
||||
#pragma mark - Unblock
|
||||
|
||||
+ (void)showUnblockThreadActionSheet:(TSThread *)thread
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock;
|
||||
|
||||
+ (void)showUnblockPhoneNumberActionSheet:(NSString *)phoneNumber
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock;
|
||||
|
||||
+ (void)showUnblockSignalAccountActionSheet:(SignalAccount *)signalAccount
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock;
|
||||
|
||||
+ (void)showUnblockGroupActionSheet:(TSGroupModel *)groupModel
|
||||
displayName:(NSString *)displayName
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock;
|
||||
|
||||
#pragma mark - UI Utils
|
||||
|
||||
+ (NSString *)formatDisplayNameForAlertTitle:(NSString *)displayName;
|
||||
+ (NSString *)formatDisplayNameForAlertMessage:(NSString *)displayName;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,494 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BlockListUIUtils.h"
|
||||
#import "TSContactThread.h"
|
||||
#import <SessionMessagingKit/OWSBlockingManager.h>
|
||||
#import <SignalUtilitiesKit/SignalAccount.h>
|
||||
#import <SessionMessagingKit/TSAccountManager.h>
|
||||
#import <SessionMessagingKit/TSGroupThread.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import "UIView+OWS.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef void (^BlockAlertCompletionBlock)(UIAlertAction *action);
|
||||
|
||||
@implementation BlockListUIUtils
|
||||
|
||||
#pragma mark - Block
|
||||
|
||||
+ (void)showBlockThreadActionSheet:(TSThread *)thread
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock
|
||||
{
|
||||
if ([thread isKindOfClass:[TSContactThread class]]) {
|
||||
TSContactThread *contactThread = (TSContactThread *)thread;
|
||||
[self showBlockPhoneNumberActionSheet:contactThread.contactSessionID
|
||||
fromViewController:fromViewController
|
||||
blockingManager:blockingManager
|
||||
completionBlock:completionBlock];
|
||||
} else if ([thread isKindOfClass:[TSGroupThread class]]) {
|
||||
TSGroupThread *groupThread = (TSGroupThread *)thread;
|
||||
[self showBlockGroupActionSheet:groupThread
|
||||
fromViewController:fromViewController
|
||||
blockingManager:blockingManager
|
||||
completionBlock:completionBlock];
|
||||
} else {
|
||||
OWSFailDebug(@"unexpected thread type: %@", thread.class);
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)showBlockPhoneNumberActionSheet:(NSString *)phoneNumber
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock
|
||||
{
|
||||
NSString *displayName = [[LKStorage.shared getContactWithSessionID:phoneNumber] displayNameFor:SNContactContextRegular] ?: phoneNumber;
|
||||
[self showBlockPhoneNumbersActionSheet:@[ phoneNumber ]
|
||||
displayName:displayName
|
||||
fromViewController:fromViewController
|
||||
blockingManager:blockingManager
|
||||
completionBlock:completionBlock];
|
||||
}
|
||||
|
||||
+ (void)showBlockSignalAccountActionSheet:(SignalAccount *)signalAccount
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock
|
||||
{
|
||||
NSString *displayName = [[LKStorage.shared getContactWithSessionID:signalAccount.recipientId] displayNameFor:SNContactContextRegular] ?: signalAccount.recipientId;
|
||||
[self showBlockPhoneNumbersActionSheet:@[ signalAccount.recipientId ]
|
||||
displayName:displayName
|
||||
fromViewController:fromViewController
|
||||
blockingManager:blockingManager
|
||||
completionBlock:completionBlock];
|
||||
}
|
||||
|
||||
+ (void)showBlockPhoneNumbersActionSheet:(NSArray<NSString *> *)phoneNumbers
|
||||
displayName:(NSString *)displayName
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock
|
||||
{
|
||||
OWSAssertDebug(phoneNumbers.count > 0);
|
||||
OWSAssertDebug(displayName.length > 0);
|
||||
OWSAssertDebug(fromViewController);
|
||||
OWSAssertDebug(blockingManager);
|
||||
|
||||
NSString *localContactId = [TSAccountManager localNumber];
|
||||
OWSAssertDebug(localContactId.length > 0);
|
||||
for (NSString *phoneNumber in phoneNumbers) {
|
||||
OWSAssertDebug(phoneNumber.length > 0);
|
||||
|
||||
if ([localContactId isEqualToString:phoneNumber]) {
|
||||
[self showOkAlertWithTitle:NSLocalizedString(@"BLOCK_LIST_VIEW_CANT_BLOCK_SELF_ALERT_TITLE",
|
||||
@"The title of the 'You can't block yourself' alert.")
|
||||
message:NSLocalizedString(@"BLOCK_LIST_VIEW_CANT_BLOCK_SELF_ALERT_MESSAGE",
|
||||
@"The message of the 'You can't block yourself' alert.")
|
||||
fromViewController:fromViewController
|
||||
completionBlock:^(UIAlertAction *action) {
|
||||
if (completionBlock) {
|
||||
completionBlock(NO);
|
||||
}
|
||||
}];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NSString *title = [NSString stringWithFormat:NSLocalizedString(@"BLOCK_LIST_BLOCK_USER_TITLE_FORMAT",
|
||||
@"A format for the 'block user' action sheet title. Embeds {{the "
|
||||
@"blocked user's name or phone number}}."),
|
||||
[self formatDisplayNameForAlertTitle:displayName]];
|
||||
|
||||
UIAlertController *actionSheet =
|
||||
[UIAlertController alertControllerWithTitle:title
|
||||
message:NSLocalizedString(@"BLOCK_USER_BEHAVIOR_EXPLANATION",
|
||||
@"An explanation of the consequences of blocking another user.")
|
||||
preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
|
||||
UIAlertAction *blockAction = [UIAlertAction
|
||||
actionWithTitle:NSLocalizedString(@"BLOCK_LIST_BLOCK_BUTTON", @"Button label for the 'block' button")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"block")
|
||||
style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[self blockPhoneNumbers:phoneNumbers
|
||||
displayName:displayName
|
||||
fromViewController:fromViewController
|
||||
blockingManager:blockingManager
|
||||
completionBlock:^(UIAlertAction *ignore) {
|
||||
if (completionBlock) {
|
||||
completionBlock(YES);
|
||||
}
|
||||
}];
|
||||
}];
|
||||
[actionSheet addAction:blockAction];
|
||||
|
||||
UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:CommonStrings.cancelButton
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"dismiss")
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
if (completionBlock) {
|
||||
completionBlock(NO);
|
||||
}
|
||||
}];
|
||||
[actionSheet addAction:dismissAction];
|
||||
[fromViewController presentAlert:actionSheet];
|
||||
}
|
||||
|
||||
+ (void)showBlockGroupActionSheet:(TSGroupThread *)groupThread
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock
|
||||
{
|
||||
OWSAssertDebug(groupThread);
|
||||
OWSAssertDebug(fromViewController);
|
||||
OWSAssertDebug(blockingManager);
|
||||
|
||||
NSString *groupName = groupThread.name.length > 0 ? groupThread.name : TSGroupThread.defaultGroupName;
|
||||
NSString *title = [NSString
|
||||
stringWithFormat:NSLocalizedString(@"BLOCK_LIST_BLOCK_GROUP_TITLE_FORMAT",
|
||||
@"A format for the 'block group' action sheet title. Embeds the {{group name}}."),
|
||||
[self formatDisplayNameForAlertTitle:groupName]];
|
||||
|
||||
UIAlertController *actionSheet =
|
||||
[UIAlertController alertControllerWithTitle:title
|
||||
message:NSLocalizedString(@"BLOCK_GROUP_BEHAVIOR_EXPLANATION",
|
||||
@"An explanation of the consequences of blocking a group.")
|
||||
preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
|
||||
UIAlertAction *blockAction = [UIAlertAction
|
||||
actionWithTitle:NSLocalizedString(@"BLOCK_LIST_BLOCK_BUTTON", @"Button label for the 'block' button")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"block")
|
||||
style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[self blockGroup:groupThread
|
||||
fromViewController:fromViewController
|
||||
blockingManager:blockingManager
|
||||
completionBlock:^(UIAlertAction *ignore) {
|
||||
if (completionBlock) {
|
||||
completionBlock(YES);
|
||||
}
|
||||
}];
|
||||
}];
|
||||
[actionSheet addAction:blockAction];
|
||||
|
||||
UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:CommonStrings.cancelButton
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"dismiss")
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
if (completionBlock) {
|
||||
completionBlock(NO);
|
||||
}
|
||||
}];
|
||||
[actionSheet addAction:dismissAction];
|
||||
[fromViewController presentAlert:actionSheet];
|
||||
}
|
||||
|
||||
+ (void)blockPhoneNumbers:(NSArray<NSString *> *)phoneNumbers
|
||||
displayName:(NSString *)displayName
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(BlockAlertCompletionBlock)completionBlock
|
||||
{
|
||||
OWSAssertDebug(phoneNumbers.count > 0);
|
||||
OWSAssertDebug(displayName.length > 0);
|
||||
OWSAssertDebug(fromViewController);
|
||||
OWSAssertDebug(blockingManager);
|
||||
|
||||
for (NSString *phoneNumber in phoneNumbers) {
|
||||
OWSAssertDebug(phoneNumber.length > 0);
|
||||
[blockingManager addBlockedPhoneNumber:phoneNumber];
|
||||
}
|
||||
|
||||
[self showOkAlertWithTitle:NSLocalizedString(
|
||||
@"BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE", @"The title of the 'user blocked' alert.")
|
||||
message:[NSString
|
||||
stringWithFormat:NSLocalizedString(@"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT",
|
||||
@"The message format of the 'conversation blocked' alert. "
|
||||
@"Embeds the {{conversation title}}."),
|
||||
[self formatDisplayNameForAlertMessage:displayName]]
|
||||
fromViewController:fromViewController
|
||||
completionBlock:completionBlock];
|
||||
}
|
||||
|
||||
+ (void)blockGroup:(TSGroupThread *)groupThread
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(BlockAlertCompletionBlock)completionBlock
|
||||
{
|
||||
OWSAssertDebug(groupThread);
|
||||
OWSAssertDebug(fromViewController);
|
||||
OWSAssertDebug(blockingManager);
|
||||
|
||||
// block the group regardless of the ability to deliver the "leave group" message.
|
||||
[blockingManager addBlockedGroup:groupThread.groupModel];
|
||||
|
||||
// blockingManager.addBlocked* creates sneaky transactions, so we can't pass in a transaction
|
||||
// via params and instead have to create our own sneaky transaction here.
|
||||
[groupThread leaveGroupWithSneakyTransaction];
|
||||
|
||||
// TODO: If we ever start using this again we should make sure to send a group leave message here
|
||||
|
||||
NSString *groupName = groupThread.name.length > 0 ? groupThread.name : TSGroupThread.defaultGroupName;
|
||||
|
||||
NSString *alertTitle
|
||||
= NSLocalizedString(@"BLOCK_LIST_VIEW_BLOCKED_GROUP_ALERT_TITLE", @"The title of the 'group blocked' alert.");
|
||||
NSString *alertBodyFormat = NSLocalizedString(@"BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT",
|
||||
@"The message format of the 'conversation blocked' alert. Embeds the {{conversation title}}.");
|
||||
NSString *alertBody =
|
||||
[NSString stringWithFormat:alertBodyFormat, [self formatDisplayNameForAlertMessage:groupName]];
|
||||
|
||||
[self showOkAlertWithTitle:alertTitle
|
||||
message:alertBody
|
||||
fromViewController:fromViewController
|
||||
completionBlock:completionBlock];
|
||||
}
|
||||
|
||||
#pragma mark - Unblock
|
||||
|
||||
+ (void)showUnblockThreadActionSheet:(TSThread *)thread
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock
|
||||
{
|
||||
if ([thread isKindOfClass:[TSContactThread class]]) {
|
||||
TSContactThread *contactThread = (TSContactThread *)thread;
|
||||
[self showUnblockPhoneNumberActionSheet:contactThread.contactSessionID
|
||||
fromViewController:fromViewController
|
||||
blockingManager:blockingManager
|
||||
completionBlock:completionBlock];
|
||||
} else if ([thread isKindOfClass:[TSGroupThread class]]) {
|
||||
TSGroupThread *groupThread = (TSGroupThread *)thread;
|
||||
NSString *groupName = groupThread.name.length > 0 ? groupThread.name : TSGroupThread.defaultGroupName;
|
||||
[self showUnblockGroupActionSheet:groupThread.groupModel
|
||||
displayName:groupName
|
||||
fromViewController:fromViewController
|
||||
blockingManager:blockingManager
|
||||
completionBlock:completionBlock];
|
||||
} else {
|
||||
OWSFailDebug(@"unexpected thread type: %@", thread.class);
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)showUnblockPhoneNumberActionSheet:(NSString *)phoneNumber
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock
|
||||
{
|
||||
NSString *displayName = [[LKStorage.shared getContactWithSessionID:phoneNumber] displayNameFor:SNContactContextRegular] ?: phoneNumber;
|
||||
[self showUnblockPhoneNumbersActionSheet:@[ phoneNumber ]
|
||||
displayName:displayName
|
||||
fromViewController:fromViewController
|
||||
blockingManager:blockingManager
|
||||
completionBlock:completionBlock];
|
||||
}
|
||||
|
||||
+ (void)showUnblockSignalAccountActionSheet:(SignalAccount *)signalAccount
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock
|
||||
{
|
||||
NSString *displayName = [[LKStorage.shared getContactWithSessionID:signalAccount.recipientId] displayNameFor:SNContactContextRegular] ?: signalAccount.recipientId;
|
||||
[self showUnblockPhoneNumbersActionSheet:@[ signalAccount.recipientId ]
|
||||
displayName:displayName
|
||||
fromViewController:fromViewController
|
||||
blockingManager:blockingManager
|
||||
completionBlock:completionBlock];
|
||||
}
|
||||
|
||||
+ (void)showUnblockPhoneNumbersActionSheet:(NSArray<NSString *> *)phoneNumbers
|
||||
displayName:(NSString *)displayName
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock
|
||||
{
|
||||
OWSAssertDebug(phoneNumbers.count > 0);
|
||||
OWSAssertDebug(displayName.length > 0);
|
||||
OWSAssertDebug(fromViewController);
|
||||
OWSAssertDebug(blockingManager);
|
||||
|
||||
NSString *title = [NSString
|
||||
stringWithFormat:
|
||||
NSLocalizedString(@"BLOCK_LIST_UNBLOCK_TITLE_FORMAT",
|
||||
@"A format for the 'unblock conversation' action sheet title. Embeds the {{conversation title}}."),
|
||||
[self formatDisplayNameForAlertTitle:displayName]];
|
||||
|
||||
UIAlertController *actionSheet =
|
||||
[UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
|
||||
UIAlertAction *unblockAction =
|
||||
[UIAlertAction actionWithTitle:NSLocalizedString(
|
||||
@"BLOCK_LIST_UNBLOCK_BUTTON", @"Button label for the 'unblock' button")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"unblock")
|
||||
style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[BlockListUIUtils unblockPhoneNumbers:phoneNumbers
|
||||
displayName:displayName
|
||||
fromViewController:fromViewController
|
||||
blockingManager:blockingManager
|
||||
completionBlock:^(UIAlertAction *ignore) {
|
||||
if (completionBlock) {
|
||||
completionBlock(NO);
|
||||
}
|
||||
}];
|
||||
}];
|
||||
[actionSheet addAction:unblockAction];
|
||||
|
||||
UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:CommonStrings.cancelButton
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"dismiss")
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
if (completionBlock) {
|
||||
completionBlock(YES);
|
||||
}
|
||||
}];
|
||||
[actionSheet addAction:dismissAction];
|
||||
[fromViewController presentAlert:actionSheet];
|
||||
}
|
||||
|
||||
+ (void)unblockPhoneNumbers:(NSArray<NSString *> *)phoneNumbers
|
||||
displayName:(NSString *)displayName
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(BlockAlertCompletionBlock)completionBlock
|
||||
{
|
||||
OWSAssertDebug(phoneNumbers.count > 0);
|
||||
OWSAssertDebug(displayName.length > 0);
|
||||
OWSAssertDebug(fromViewController);
|
||||
OWSAssertDebug(blockingManager);
|
||||
|
||||
for (NSString *phoneNumber in phoneNumbers) {
|
||||
OWSAssertDebug(phoneNumber.length > 0);
|
||||
[blockingManager removeBlockedPhoneNumber:phoneNumber];
|
||||
}
|
||||
|
||||
NSString *titleFormat = NSLocalizedString(@"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT",
|
||||
@"Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}.");
|
||||
NSString *title = [NSString stringWithFormat:titleFormat, [self formatDisplayNameForAlertMessage:displayName]];
|
||||
|
||||
[self showOkAlertWithTitle:title message:nil fromViewController:fromViewController completionBlock:completionBlock];
|
||||
}
|
||||
|
||||
+ (void)showUnblockGroupActionSheet:(TSGroupModel *)groupModel
|
||||
displayName:(NSString *)displayName
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(nullable BlockActionCompletionBlock)completionBlock
|
||||
{
|
||||
OWSAssertDebug(displayName.length > 0);
|
||||
OWSAssertDebug(fromViewController);
|
||||
OWSAssertDebug(blockingManager);
|
||||
|
||||
NSString *title =
|
||||
[NSString stringWithFormat:NSLocalizedString(@"BLOCK_LIST_UNBLOCK_GROUP_TITLE",
|
||||
@"Action sheet title when confirming you want to unblock a group.")];
|
||||
|
||||
NSString *message = NSLocalizedString(
|
||||
@"BLOCK_LIST_UNBLOCK_GROUP_BODY", @"Action sheet body when confirming you want to unblock a group");
|
||||
|
||||
UIAlertController *actionSheet = [UIAlertController alertControllerWithTitle:title
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
|
||||
UIAlertAction *unblockAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"BLOCK_LIST_UNBLOCK_BUTTON",
|
||||
@"Button label for the 'unblock' button")
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"unblock")
|
||||
style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[BlockListUIUtils unblockGroup:groupModel
|
||||
displayName:displayName
|
||||
fromViewController:fromViewController
|
||||
blockingManager:blockingManager
|
||||
completionBlock:^(UIAlertAction *ignore) {
|
||||
if (completionBlock) {
|
||||
completionBlock(NO);
|
||||
}
|
||||
}];
|
||||
}];
|
||||
[actionSheet addAction:unblockAction];
|
||||
|
||||
UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:CommonStrings.cancelButton
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"dismiss")
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
if (completionBlock) {
|
||||
completionBlock(YES);
|
||||
}
|
||||
}];
|
||||
[actionSheet addAction:dismissAction];
|
||||
[fromViewController presentAlert:actionSheet];
|
||||
}
|
||||
|
||||
+ (void)unblockGroup:(TSGroupModel *)groupModel
|
||||
displayName:(NSString *)displayName
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
blockingManager:(OWSBlockingManager *)blockingManager
|
||||
completionBlock:(BlockAlertCompletionBlock)completionBlock
|
||||
{
|
||||
OWSAssertDebug(displayName.length > 0);
|
||||
OWSAssertDebug(fromViewController);
|
||||
OWSAssertDebug(blockingManager);
|
||||
|
||||
[blockingManager removeBlockedGroupId:groupModel.groupId];
|
||||
|
||||
NSString *titleFormat = NSLocalizedString(@"BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT",
|
||||
@"Alert title after unblocking a group or 1:1 chat. Embeds the {{conversation title}}.");
|
||||
NSString *title = [NSString stringWithFormat:titleFormat, [self formatDisplayNameForAlertMessage:displayName]];
|
||||
|
||||
NSString *message
|
||||
= NSLocalizedString(@"BLOCK_LIST_VIEW_UNBLOCKED_GROUP_ALERT_BODY", @"Alert body after unblocking a group.");
|
||||
[self showOkAlertWithTitle:title
|
||||
message:message
|
||||
fromViewController:fromViewController
|
||||
completionBlock:completionBlock];
|
||||
}
|
||||
|
||||
#pragma mark - UI
|
||||
|
||||
+ (void)showOkAlertWithTitle:(NSString *)title
|
||||
message:(nullable NSString *)message
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
completionBlock:(BlockAlertCompletionBlock)completionBlock
|
||||
{
|
||||
OWSAssertDebug(title.length > 0);
|
||||
OWSAssertDebug(fromViewController);
|
||||
|
||||
UIAlertController *alert =
|
||||
[UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
UIAlertAction *okAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"BUTTON_OK", nil)
|
||||
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"ok")
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:completionBlock];
|
||||
[alert addAction:okAction];
|
||||
[fromViewController presentAlert:alert];
|
||||
}
|
||||
|
||||
+ (NSString *)formatDisplayNameForAlertTitle:(NSString *)displayName
|
||||
{
|
||||
return [self formatDisplayName:displayName withMaxLength:20];
|
||||
}
|
||||
|
||||
+ (NSString *)formatDisplayNameForAlertMessage:(NSString *)displayName
|
||||
{
|
||||
return [self formatDisplayName:displayName withMaxLength:127];
|
||||
}
|
||||
|
||||
+ (NSString *)formatDisplayName:(NSString *)displayName withMaxLength:(NSUInteger)maxLength
|
||||
{
|
||||
OWSAssertDebug(displayName.length > 0);
|
||||
|
||||
if (displayName.length > maxLength) {
|
||||
return [[displayName substringToIndex:maxLength] stringByAppendingString:@"…"];
|
||||
}
|
||||
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -0,0 +1,144 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionMessagingKit
|
||||
|
||||
@objc public class BlockListUIUtils: NSObject {
|
||||
// MARK: - Block
|
||||
|
||||
/// This method shows an alert to unblock a contact in a ContactThread and will update the `isBlocked` flag of the contact if the user decides to continue
|
||||
///
|
||||
/// **Note:** Make sure to force a config sync in the `completionBlock` if the blocked state was successfully changed
|
||||
@objc public static func showBlockThreadActionSheet(_ thread: TSContactThread, from viewController: UIViewController, completionBlock: ((Bool) -> ())? = nil) {
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
|
||||
guard thread.contactSessionID() != userPublicKey, let contact: Contact = Storage.shared.getContact(with: thread.contactSessionID()) else {
|
||||
completionBlock?(false)
|
||||
return
|
||||
}
|
||||
|
||||
let displayName: String = (contact.displayName(for: .regular) ?? thread.contactSessionID())
|
||||
let actionSheet: UIAlertController = UIAlertController(
|
||||
title: String(
|
||||
format: "BLOCK_LIST_BLOCK_USER_TITLE_FORMAT".localized(),
|
||||
self.formatForAlertTitle(displayName: displayName)
|
||||
),
|
||||
message: "BLOCK_USER_BEHAVIOR_EXPLANATION".localized(),
|
||||
preferredStyle: .actionSheet
|
||||
)
|
||||
actionSheet.addAction(UIAlertAction(
|
||||
title: "BLOCK_LIST_BLOCK_BUTTON".localized(),
|
||||
accessibilityIdentifier: "\(type(of: self).self).block",
|
||||
style: .destructive,
|
||||
handler: { _ in
|
||||
Storage.write(
|
||||
with: { transaction in
|
||||
contact.isBlocked = true
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
},
|
||||
completion: {
|
||||
self.showOkAlert(
|
||||
title: "BLOCK_LIST_VIEW_BLOCKED_ALERT_TITLE".localized(),
|
||||
message: String(
|
||||
format: "BLOCK_LIST_VIEW_BLOCKED_ALERT_MESSAGE_FORMAT".localized(),
|
||||
self.formatForAlertMessage(displayName: displayName)
|
||||
),
|
||||
from: viewController,
|
||||
completionBlock: { _ in completionBlock?(true) }
|
||||
)
|
||||
})
|
||||
}
|
||||
))
|
||||
actionSheet.addAction(UIAlertAction(
|
||||
title: CommonStrings.cancelButton,
|
||||
accessibilityIdentifier: "\(type(of: self).self).dismiss",
|
||||
style: .cancel,
|
||||
handler: { _ in completionBlock?(false) }
|
||||
))
|
||||
|
||||
viewController.presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
// MARK: - Unblock
|
||||
|
||||
/// This method shows an alert to unblock a contact in a ContactThread and will update the `isBlocked` flag of the contact if the user decides to continue
|
||||
///
|
||||
/// **Note:** Make sure to force a config sync in the `completionBlock` if the blocked state was successfully changed
|
||||
@objc public static func showUnblockThreadActionSheet(_ thread: TSContactThread, from viewController: UIViewController, completionBlock: ((Bool) -> ())? = nil) {
|
||||
guard let contact: Contact = Storage.shared.getContact(with: thread.contactSessionID()) else {
|
||||
completionBlock?(true)
|
||||
return
|
||||
}
|
||||
|
||||
let displayName: String = (contact.displayName(for: .regular) ?? thread.contactSessionID())
|
||||
let actionSheet: UIAlertController = UIAlertController(
|
||||
title: String(
|
||||
format: "BLOCK_LIST_UNBLOCK_TITLE_FORMAT".localized(),
|
||||
self.formatForAlertTitle(displayName: displayName)
|
||||
),
|
||||
message: nil,
|
||||
preferredStyle: .actionSheet
|
||||
)
|
||||
actionSheet.addAction(UIAlertAction(
|
||||
title: "BLOCK_LIST_UNBLOCK_BUTTON".localized(),
|
||||
accessibilityIdentifier: "\(type(of: self).self).unblock",
|
||||
style: .destructive,
|
||||
handler: { _ in
|
||||
Storage.write(
|
||||
with: { transaction in
|
||||
contact.isBlocked = false
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
},
|
||||
completion: {
|
||||
self.showOkAlert(
|
||||
title: String(
|
||||
format: "BLOCK_LIST_VIEW_UNBLOCKED_ALERT_TITLE_FORMAT".localized(),
|
||||
self.formatForAlertMessage(displayName: displayName)
|
||||
),
|
||||
message: nil,
|
||||
from: viewController,
|
||||
completionBlock: { _ in completionBlock?(false) }
|
||||
)
|
||||
})
|
||||
}
|
||||
))
|
||||
actionSheet.addAction(UIAlertAction(
|
||||
title: CommonStrings.cancelButton,
|
||||
accessibilityIdentifier: "\(type(of: self).self).dismiss",
|
||||
style: .cancel,
|
||||
handler: { _ in completionBlock?(true) }
|
||||
))
|
||||
|
||||
viewController.presentAlert(actionSheet)
|
||||
}
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
@objc public static func showOkAlert(title: String, message: String?, from viewController: UIViewController, completionBlock: @escaping (UIAlertAction) -> ()) {
|
||||
let alertController: UIAlertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(
|
||||
title: "BUTTON_OK".localized(),
|
||||
accessibilityIdentifier: "\(type(of: self).self).ok",
|
||||
style: .default,
|
||||
handler: completionBlock
|
||||
))
|
||||
|
||||
viewController.presentAlert(alertController)
|
||||
}
|
||||
|
||||
@objc public static func formatForAlertTitle(displayName: String) -> String {
|
||||
return format(displayName: displayName, maxLength: 20)
|
||||
}
|
||||
|
||||
@objc public static func formatForAlertMessage(displayName: String) -> String {
|
||||
return format(displayName: displayName, maxLength: 127)
|
||||
}
|
||||
|
||||
@objc public static func format(displayName: String, maxLength: Int) -> String {
|
||||
guard displayName.count <= maxLength else {
|
||||
return "\(displayName.substring(to: maxLength))…"
|
||||
}
|
||||
|
||||
return displayName
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class SignalAccount;
|
||||
|
||||
@protocol SelectRecipientViewControllerDelegate <NSObject>
|
||||
|
||||
- (NSString *)phoneNumberSectionTitle;
|
||||
- (NSString *)phoneNumberButtonText;
|
||||
- (NSString *)contactsSectionTitle;
|
||||
|
||||
- (void)phoneNumberWasSelected:(NSString *)phoneNumber;
|
||||
|
||||
- (BOOL)canSignalAccountBeSelected:(SignalAccount *)signalAccount;
|
||||
|
||||
- (void)signalAccountWasSelected:(SignalAccount *)signalAccount;
|
||||
|
||||
- (nullable NSString *)accessoryMessageForSignalAccount:(SignalAccount *)signalAccount;
|
||||
|
||||
- (BOOL)shouldHideLocalNumber;
|
||||
|
||||
- (BOOL)shouldHideContacts;
|
||||
|
||||
- (BOOL)shouldValidatePhoneNumbers;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@class ContactsViewHelper;
|
||||
|
||||
@interface SelectRecipientViewController : OWSViewController
|
||||
|
||||
@property (nonatomic, weak) id<SelectRecipientViewControllerDelegate> delegate;
|
||||
|
||||
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
|
||||
|
||||
@property (nonatomic) BOOL isPresentedInNavigationController;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,332 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/SelectRecipientViewController.h>
|
||||
|
||||
#import <SignalUtilitiesKit/ContactTableViewCell.h>
|
||||
#import <SignalUtilitiesKit/OWSTableViewController.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/UIFont+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/AppContext.h>
|
||||
#import <SignalUtilitiesKit/SignalAccount.h>
|
||||
#import <SessionMessagingKit/TSAccountManager.h>
|
||||
#import <SignalUtilitiesKit/OWSTextField.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSString *const kSelectRecipientViewControllerCellIdentifier = @"kSelectRecipientViewControllerCellIdentifier";
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface SelectRecipientViewController () </*CountryCodeViewControllerDelegate,*/
|
||||
OWSTableViewControllerDelegate,
|
||||
UITextFieldDelegate>
|
||||
|
||||
@property (nonatomic) UIButton *countryCodeButton;
|
||||
|
||||
@property (nonatomic) UITextField *phoneNumberTextField;
|
||||
|
||||
@property (nonatomic) OWSFlatButton *phoneNumberButton;
|
||||
|
||||
@property (nonatomic) UILabel *examplePhoneNumberLabel;
|
||||
|
||||
@property (nonatomic, readonly) OWSTableViewController *tableViewController;
|
||||
|
||||
@property (nonatomic) NSString *callingCode;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation SelectRecipientViewController
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
self.view.backgroundColor = [Theme backgroundColor];
|
||||
|
||||
[self createViews];
|
||||
|
||||
if (self.delegate.shouldHideContacts) {
|
||||
self.tableViewController.tableView.scrollEnabled = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
OWSAssertDebug(self.tableViewController);
|
||||
|
||||
[super viewDidLoad];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
[self.tableViewController viewDidAppear:animated];
|
||||
|
||||
if ([self.delegate shouldHideContacts]) {
|
||||
[self.phoneNumberTextField becomeFirstResponder];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)createViews
|
||||
{
|
||||
OWSAssertDebug(self.delegate);
|
||||
|
||||
_tableViewController = [OWSTableViewController new];
|
||||
_tableViewController.delegate = self;
|
||||
[self.view addSubview:self.tableViewController.view];
|
||||
[self.tableViewController.view autoPinEdgeToSuperviewSafeArea:ALEdgeLeading];
|
||||
[self.tableViewController.view autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing];
|
||||
[_tableViewController.view autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:self.view withOffset:0];
|
||||
[_tableViewController.view autoPinEdgeToSuperviewEdge:ALEdgeBottom];
|
||||
self.tableViewController.tableView.rowHeight = UITableViewAutomaticDimension;
|
||||
self.tableViewController.tableView.estimatedRowHeight = 60;
|
||||
_tableViewController.view.backgroundColor = [Theme backgroundColor];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (UILabel *)countryCodeLabel
|
||||
{
|
||||
UILabel *countryCodeLabel = [UILabel new];
|
||||
countryCodeLabel.font = [UIFont ows_mediumFontWithSize:18.f];
|
||||
countryCodeLabel.textColor = [Theme primaryColor];
|
||||
countryCodeLabel.text
|
||||
= NSLocalizedString(@"REGISTRATION_DEFAULT_COUNTRY_NAME", @"Label for the country code field");
|
||||
return countryCodeLabel;
|
||||
}
|
||||
|
||||
- (UIButton *)countryCodeButton
|
||||
{
|
||||
if (!_countryCodeButton) {
|
||||
_countryCodeButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
_countryCodeButton.titleLabel.font = [UIFont ows_mediumFontWithSize:18.f];
|
||||
_countryCodeButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight;
|
||||
[_countryCodeButton setTitleColor:[UIColor ows_materialBlueColor] forState:UIControlStateNormal];
|
||||
[_countryCodeButton addTarget:self
|
||||
action:@selector(showCountryCodeView:)
|
||||
forControlEvents:UIControlEventTouchUpInside];
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _countryCodeButton);
|
||||
}
|
||||
|
||||
return _countryCodeButton;
|
||||
}
|
||||
|
||||
- (UILabel *)phoneNumberLabel
|
||||
{
|
||||
UILabel *phoneNumberLabel = [UILabel new];
|
||||
phoneNumberLabel.font = [UIFont ows_mediumFontWithSize:18.f];
|
||||
phoneNumberLabel.textColor = [Theme primaryColor];
|
||||
phoneNumberLabel.text
|
||||
= NSLocalizedString(@"REGISTRATION_PHONENUMBER_BUTTON", @"Label for the phone number textfield");
|
||||
return phoneNumberLabel;
|
||||
}
|
||||
|
||||
- (UIFont *)examplePhoneNumberFont
|
||||
{
|
||||
return [UIFont ows_regularFontWithSize:16.f];
|
||||
}
|
||||
|
||||
- (UILabel *)examplePhoneNumberLabel
|
||||
{
|
||||
if (!_examplePhoneNumberLabel) {
|
||||
_examplePhoneNumberLabel = [UILabel new];
|
||||
_examplePhoneNumberLabel.font = [self examplePhoneNumberFont];
|
||||
_examplePhoneNumberLabel.textColor = [Theme secondaryColor];
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _examplePhoneNumberLabel);
|
||||
}
|
||||
|
||||
return _examplePhoneNumberLabel;
|
||||
}
|
||||
|
||||
- (UITextField *)phoneNumberTextField
|
||||
{
|
||||
if (!_phoneNumberTextField) {
|
||||
_phoneNumberTextField = [OWSTextField new];
|
||||
_phoneNumberTextField.font = [UIFont ows_mediumFontWithSize:18.f];
|
||||
_phoneNumberTextField.textAlignment = _phoneNumberTextField.textAlignmentUnnatural;
|
||||
_phoneNumberTextField.textColor = [UIColor ows_materialBlueColor];
|
||||
_phoneNumberTextField.placeholder = NSLocalizedString(
|
||||
@"REGISTRATION_ENTERNUMBER_DEFAULT_TEXT", @"Placeholder text for the phone number textfield");
|
||||
_phoneNumberTextField.keyboardType = UIKeyboardTypeNumberPad;
|
||||
_phoneNumberTextField.delegate = self;
|
||||
[_phoneNumberTextField addTarget:self
|
||||
action:@selector(textFieldDidChange:)
|
||||
forControlEvents:UIControlEventEditingChanged];
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _phoneNumberTextField);
|
||||
}
|
||||
|
||||
return _phoneNumberTextField;
|
||||
}
|
||||
|
||||
- (OWSFlatButton *)phoneNumberButton
|
||||
{
|
||||
if (!_phoneNumberButton) {
|
||||
const CGFloat kButtonHeight = 40;
|
||||
OWSFlatButton *button = [OWSFlatButton buttonWithTitle:[self.delegate phoneNumberButtonText]
|
||||
font:[OWSFlatButton fontForHeight:kButtonHeight]
|
||||
titleColor:[UIColor whiteColor]
|
||||
backgroundColor:[UIColor ows_materialBlueColor]
|
||||
target:self
|
||||
selector:@selector(phoneNumberButtonPressed)];
|
||||
_phoneNumberButton = button;
|
||||
[button autoSetDimension:ALDimensionWidth toSize:140];
|
||||
[button autoSetDimension:ALDimensionHeight toSize:kButtonHeight];
|
||||
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _phoneNumberButton);
|
||||
}
|
||||
return _phoneNumberButton;
|
||||
}
|
||||
|
||||
- (UIView *)createRowWithHeight:(CGFloat)height
|
||||
previousRow:(nullable UIView *)previousRow
|
||||
superview:(nullable UIView *)superview
|
||||
{
|
||||
UIView *row = [UIView containerView];
|
||||
[superview addSubview:row];
|
||||
[row autoPinLeadingAndTrailingToSuperviewMargin];
|
||||
if (previousRow) {
|
||||
[row autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:previousRow withOffset:0];
|
||||
} else {
|
||||
[row autoPinEdgeToSuperviewEdge:ALEdgeTop];
|
||||
}
|
||||
[row autoSetDimension:ALDimensionHeight toSize:height];
|
||||
return row;
|
||||
}
|
||||
|
||||
#pragma mark - UITextFieldDelegate
|
||||
|
||||
- (BOOL)textField:(UITextField *)textField
|
||||
shouldChangeCharactersInRange:(NSRange)range
|
||||
replacementString:(NSString *)insertionText
|
||||
{
|
||||
return NO; // inform our caller that we took care of performing the change
|
||||
}
|
||||
|
||||
- (BOOL)textFieldShouldReturn:(UITextField *)textField
|
||||
{
|
||||
[textField resignFirstResponder];
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - Table Contents
|
||||
|
||||
- (void)updateTableContents
|
||||
{
|
||||
OWSTableContents *contents = [OWSTableContents new];
|
||||
__weak SelectRecipientViewController *weakSelf = self;
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
|
||||
OWSTableSection *phoneNumberSection = [OWSTableSection new];
|
||||
phoneNumberSection.headerTitle = [self.delegate phoneNumberSectionTitle];
|
||||
const CGFloat kCountryRowHeight = 50;
|
||||
const CGFloat kPhoneNumberRowHeight = 50;
|
||||
const CGFloat examplePhoneNumberRowHeight = self.examplePhoneNumberFont.lineHeight + 3.f;
|
||||
const CGFloat kButtonRowHeight = 60;
|
||||
[phoneNumberSection addItem:[OWSTableItem itemWithCustomCellBlock:^{
|
||||
SelectRecipientViewController *strongSelf = weakSelf;
|
||||
OWSCAssertDebug(strongSelf);
|
||||
|
||||
UITableViewCell *cell = [OWSTableItem newCell];
|
||||
cell.preservesSuperviewLayoutMargins = YES;
|
||||
cell.contentView.preservesSuperviewLayoutMargins = YES;
|
||||
|
||||
// Country Row
|
||||
UIView *countryRow =
|
||||
[strongSelf createRowWithHeight:kCountryRowHeight previousRow:nil superview:cell.contentView];
|
||||
[countryRow addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:strongSelf
|
||||
action:@selector(countryRowTouched:)]];
|
||||
|
||||
UILabel *countryCodeLabel = strongSelf.countryCodeLabel;
|
||||
[countryRow addSubview:countryCodeLabel];
|
||||
[countryCodeLabel autoPinLeadingToSuperviewMargin];
|
||||
[countryCodeLabel autoVCenterInSuperview];
|
||||
|
||||
[countryRow addSubview:strongSelf.countryCodeButton];
|
||||
[strongSelf.countryCodeButton autoPinTrailingToSuperviewMargin];
|
||||
[strongSelf.countryCodeButton autoVCenterInSuperview];
|
||||
|
||||
// Phone Number Row
|
||||
UIView *phoneNumberRow =
|
||||
[strongSelf createRowWithHeight:kPhoneNumberRowHeight previousRow:countryRow superview:cell.contentView];
|
||||
[phoneNumberRow
|
||||
addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:strongSelf
|
||||
action:@selector(phoneNumberRowTouched:)]];
|
||||
|
||||
UILabel *phoneNumberLabel = strongSelf.phoneNumberLabel;
|
||||
[phoneNumberRow addSubview:phoneNumberLabel];
|
||||
[phoneNumberLabel autoPinLeadingToSuperviewMargin];
|
||||
[phoneNumberLabel autoVCenterInSuperview];
|
||||
|
||||
[phoneNumberRow addSubview:strongSelf.phoneNumberTextField];
|
||||
[strongSelf.phoneNumberTextField autoPinLeadingToTrailingEdgeOfView:phoneNumberLabel offset:10.f];
|
||||
[strongSelf.phoneNumberTextField autoPinTrailingToSuperviewMargin];
|
||||
[strongSelf.phoneNumberTextField autoVCenterInSuperview];
|
||||
|
||||
// Example row.
|
||||
UIView *examplePhoneNumberRow = [strongSelf createRowWithHeight:examplePhoneNumberRowHeight
|
||||
previousRow:phoneNumberRow
|
||||
superview:cell.contentView];
|
||||
[examplePhoneNumberRow addSubview:strongSelf.examplePhoneNumberLabel];
|
||||
[strongSelf.examplePhoneNumberLabel autoVCenterInSuperview];
|
||||
[strongSelf.examplePhoneNumberLabel autoPinTrailingToSuperviewMargin];
|
||||
|
||||
// Phone Number Button Row
|
||||
UIView *buttonRow = [strongSelf createRowWithHeight:kButtonRowHeight
|
||||
previousRow:examplePhoneNumberRow
|
||||
superview:cell.contentView];
|
||||
[buttonRow addSubview:strongSelf.phoneNumberButton];
|
||||
[strongSelf.phoneNumberButton autoVCenterInSuperview];
|
||||
[strongSelf.phoneNumberButton autoPinTrailingToSuperviewMargin];
|
||||
|
||||
[buttonRow autoPinEdgeToSuperviewEdge:ALEdgeBottom];
|
||||
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
return cell;
|
||||
}
|
||||
customRowHeight:kCountryRowHeight + kPhoneNumberRowHeight
|
||||
+ examplePhoneNumberRowHeight + kButtonRowHeight
|
||||
actionBlock:nil]];
|
||||
[contents addSection:phoneNumberSection];
|
||||
|
||||
self.tableViewController.contents = contents;
|
||||
}
|
||||
|
||||
- (void)phoneNumberRowTouched:(UIGestureRecognizer *)sender
|
||||
{
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
[self.phoneNumberTextField becomeFirstResponder];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)countryRowTouched:(UIGestureRecognizer *)sender
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
#pragma mark - OWSTableViewControllerDelegate
|
||||
|
||||
- (void)tableViewWillBeginDragging
|
||||
{
|
||||
[self.phoneNumberTextField resignFirstResponder];
|
||||
}
|
||||
|
||||
#pragma mark - ContactsViewHelperDelegate
|
||||
|
||||
- (void)contactsViewHelperDidUpdateContacts
|
||||
{
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (BOOL)shouldHideLocalNumber
|
||||
{
|
||||
return [self.delegate shouldHideLocalNumber];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,31 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSViewController.h>
|
||||
|
||||
@class TSThread;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol SelectThreadViewControllerDelegate <NSObject>
|
||||
|
||||
- (void)threadWasSelected:(TSThread *)thread;
|
||||
|
||||
- (BOOL)canSelectBlockedContact;
|
||||
|
||||
- (nullable UIView *)createHeaderWithSearchBar:(UISearchBar *)searchBar;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
// A base class for views used to pick a single signal user, either by
|
||||
// entering a phone number or picking from your contacts.
|
||||
@interface SelectThreadViewController : OWSViewController
|
||||
|
||||
@property (nonatomic, weak) id<SelectThreadViewControllerDelegate> selectThreadViewDelegate;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,315 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SelectThreadViewController.h"
|
||||
#import "ContactTableViewCell.h"
|
||||
#import "OWSSearchBar.h"
|
||||
#import "OWSTableViewController.h"
|
||||
#import "ThreadViewHelper.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||
#import <SignalUtilitiesKit/SignalAccount.h>
|
||||
#import <SessionMessagingKit/TSAccountManager.h>
|
||||
#import <SessionMessagingKit/TSContactThread.h>
|
||||
#import <SessionMessagingKit/TSThread.h>
|
||||
#import <YapDatabase/YapDatabase.h>
|
||||
#import <SessionUIKit/SessionUIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SelectThreadViewController () <OWSTableViewControllerDelegate,
|
||||
ThreadViewHelperDelegate,
|
||||
UISearchBarDelegate>
|
||||
|
||||
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
|
||||
@property (nonatomic, readonly) FullTextSearcher *fullTextSearcher;
|
||||
@property (nonatomic, readonly) ThreadViewHelper *threadViewHelper;
|
||||
@property (nonatomic, readonly) YapDatabaseConnection *uiDatabaseConnection;
|
||||
|
||||
@property (nonatomic, readonly) OWSTableViewController *tableViewController;
|
||||
|
||||
@property (nonatomic, readonly) UISearchBar *searchBar;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation SelectThreadViewController
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
UIBarButtonItem *closeButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"X"] style:UIBarButtonItemStylePlain target:self action:@selector(dismissPressed:)];
|
||||
closeButton.tintColor = LKColors.text;
|
||||
self.navigationItem.leftBarButtonItem = closeButton;
|
||||
|
||||
_fullTextSearcher = FullTextSearcher.shared;
|
||||
_threadViewHelper = [ThreadViewHelper new];
|
||||
_threadViewHelper.delegate = self;
|
||||
|
||||
_uiDatabaseConnection = [[OWSPrimaryStorage sharedManager] newDatabaseConnection];
|
||||
#ifdef DEBUG
|
||||
_uiDatabaseConnection.permittedTransactions = YDB_AnyReadTransaction;
|
||||
#endif
|
||||
[_uiDatabaseConnection beginLongLivedReadTransaction];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(yapDatabaseModified:)
|
||||
name:YapDatabaseModifiedNotification
|
||||
object:OWSPrimaryStorage.sharedManager.dbNotificationObject];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(yapDatabaseModifiedExternally:)
|
||||
name:YapDatabaseModifiedExternallyNotification
|
||||
object:nil];
|
||||
|
||||
[self createViews];
|
||||
|
||||
// Loki: Set gradient background
|
||||
self.tableViewController.tableView.backgroundColor = UIColor.clearColor;
|
||||
self.tableViewController.view.backgroundColor = UIColor.clearColor;
|
||||
CAGradientLayer *layer = [CAGradientLayer new];
|
||||
layer.frame = UIScreen.mainScreen.bounds;
|
||||
UIColor *gradientStartColor = LKAppModeUtilities.isLightMode ? [UIColor colorWithRGBHex:0xFCFCFC] : [UIColor colorWithRGBHex:0x171717];
|
||||
UIColor *gradientEndColor = LKAppModeUtilities.isLightMode ? [UIColor colorWithRGBHex:0xFFFFFF] : [UIColor colorWithRGBHex:0x121212];
|
||||
layer.colors = @[ (id)gradientStartColor.CGColor, (id)gradientEndColor.CGColor ];
|
||||
[self.tableViewController.view.layer insertSublayer:layer atIndex:0];
|
||||
|
||||
// Loki: Set navigation bar background color
|
||||
UINavigationBar *navigationBar = self.navigationController.navigationBar;
|
||||
[navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
|
||||
navigationBar.shadowImage = [UIImage new];
|
||||
[navigationBar setTranslucent:NO];
|
||||
navigationBar.barTintColor = LKColors.navigationBarBackground;
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)createViews
|
||||
{
|
||||
OWSAssertDebug(self.selectThreadViewDelegate);
|
||||
|
||||
// Table
|
||||
_tableViewController = [OWSTableViewController new];
|
||||
_tableViewController.delegate = self;
|
||||
[self.view addSubview:self.tableViewController.view];
|
||||
[self.tableViewController.view autoPinEdgeToSuperviewSafeArea:ALEdgeLeading];
|
||||
[self.tableViewController.view autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing];
|
||||
[_tableViewController.view autoPinEdgeToSuperviewEdge:ALEdgeTop];
|
||||
[_tableViewController.view autoPinEdgeToSuperviewEdge:ALEdgeBottom];
|
||||
self.tableViewController.tableView.rowHeight = UITableViewAutomaticDimension;
|
||||
self.tableViewController.tableView.estimatedRowHeight = 60;
|
||||
BOOL isIphone6OrSmaller = (UIScreen.mainScreen.bounds.size.height - 667) < 1;
|
||||
CGFloat bottomInset = isIphone6OrSmaller ? LKValues.mediumSpacing : 34.0f;
|
||||
self.tableViewController.tableView.contentInset = UIEdgeInsetsMake(0, 0, bottomInset, 0);
|
||||
}
|
||||
|
||||
- (void)yapDatabaseModifiedExternally:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
[self.uiDatabaseConnection beginLongLivedReadTransaction];
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)yapDatabaseModified:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self.uiDatabaseConnection beginLongLivedReadTransaction];
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - UISearchBarDelegate
|
||||
|
||||
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
|
||||
{
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
|
||||
{
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
|
||||
{
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)searchBarResultsListButtonClicked:(UISearchBar *)searchBar
|
||||
{
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope
|
||||
{
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - Table Contents
|
||||
|
||||
- (void)updateTableContents
|
||||
{
|
||||
__weak SelectThreadViewController *weakSelf = self;
|
||||
ContactsViewHelper *helper = self.contactsViewHelper;
|
||||
OWSTableContents *contents = [OWSTableContents new];
|
||||
|
||||
// Existing threads are listed first, ordered by most recently active
|
||||
OWSTableSection *recentChatsSection = [OWSTableSection new];
|
||||
recentChatsSection.headerTitle = NSLocalizedString(@"SELECT_THREAD_TABLE_RECENT_CHATS_TITLE", @"");
|
||||
for (TSThread *thread in self.threadViewHelper.threads) {
|
||||
[recentChatsSection
|
||||
addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
SelectThreadViewController *strongSelf = weakSelf;
|
||||
OWSCAssertDebug(strongSelf);
|
||||
|
||||
// To be consistent with the threads (above), we use ContactTableViewCell
|
||||
// instead of HomeViewCell to present contacts and threads.
|
||||
ContactTableViewCell *cell = [ContactTableViewCell new];
|
||||
|
||||
BOOL isBlocked = [SSKEnvironment.shared.blockingManager isThreadBlocked:thread];
|
||||
if (isBlocked) {
|
||||
cell.accessoryMessage = NSLocalizedString(
|
||||
@"CONTACT_CELL_IS_BLOCKED", @"An indicator that a contact has been blocked.");
|
||||
}
|
||||
|
||||
[cell configureWithThread:thread];
|
||||
|
||||
if (!cell.hasAccessoryText) {
|
||||
// Don't add a disappearing messages indicator if we've already added a "blocked" label.
|
||||
__block OWSDisappearingMessagesConfiguration *disappearingMessagesConfiguration;
|
||||
[self.uiDatabaseConnection
|
||||
readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
|
||||
disappearingMessagesConfiguration = [OWSDisappearingMessagesConfiguration
|
||||
fetchObjectWithUniqueID:thread.uniqueId
|
||||
transaction:transaction];
|
||||
}];
|
||||
|
||||
if (disappearingMessagesConfiguration && disappearingMessagesConfiguration.isEnabled) {
|
||||
DisappearingTimerConfigurationView *disappearingTimerConfigurationView =
|
||||
[[DisappearingTimerConfigurationView alloc]
|
||||
initWithDurationSeconds:disappearingMessagesConfiguration.durationSeconds];
|
||||
|
||||
disappearingTimerConfigurationView.tintColor = LKColors.text;
|
||||
[disappearingTimerConfigurationView autoSetDimensionsToSize:CGSizeMake(44, 44)];
|
||||
|
||||
[cell ows_setAccessoryView:disappearingTimerConfigurationView];
|
||||
}
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
customRowHeight:UITableViewAutomaticDimension
|
||||
actionBlock:^{
|
||||
typeof(self) strongSelf = weakSelf;
|
||||
if (!strongSelf) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL isBlocked = [SSKEnvironment.shared.blockingManager isThreadBlocked:thread];
|
||||
if (isBlocked && ![strongSelf.selectThreadViewDelegate canSelectBlockedContact]) {
|
||||
[BlockListUIUtils
|
||||
showUnblockThreadActionSheet:thread
|
||||
fromViewController:strongSelf
|
||||
blockingManager:SSKEnvironment.shared.blockingManager
|
||||
completionBlock:^(BOOL isStillBlocked) {
|
||||
if (!isStillBlocked) {
|
||||
[strongSelf.selectThreadViewDelegate threadWasSelected:thread];
|
||||
}
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
[strongSelf.selectThreadViewDelegate threadWasSelected:thread];
|
||||
}]];
|
||||
}
|
||||
|
||||
if (recentChatsSection.itemCount > 0) {
|
||||
[contents addSection:recentChatsSection];
|
||||
}
|
||||
|
||||
if (recentChatsSection.itemCount < 1) {
|
||||
OWSTableSection *emptySection = [OWSTableSection new];
|
||||
[emptySection
|
||||
addItem:[OWSTableItem
|
||||
softCenterLabelItemWithText:NSLocalizedString(@"SETTINGS_BLOCK_LIST_NO_CONTACTS",
|
||||
@"A label that indicates the user has no Signal contacts.")]];
|
||||
[contents addSection:emptySection];
|
||||
}
|
||||
|
||||
self.tableViewController.contents = contents;
|
||||
}
|
||||
|
||||
- (void)signalAccountWasSelected:(SignalAccount *)signalAccount
|
||||
{
|
||||
OWSAssertDebug(signalAccount);
|
||||
OWSAssertDebug(self.selectThreadViewDelegate);
|
||||
|
||||
if ([SSKEnvironment.shared.blockingManager isRecipientIdBlocked:signalAccount.recipientId]
|
||||
&& ![self.selectThreadViewDelegate canSelectBlockedContact]) {
|
||||
|
||||
__weak SelectThreadViewController *weakSelf = self;
|
||||
[BlockListUIUtils showUnblockSignalAccountActionSheet:signalAccount
|
||||
fromViewController:self
|
||||
blockingManager:SSKEnvironment.shared.blockingManager
|
||||
completionBlock:^(BOOL isBlocked) {
|
||||
if (!isBlocked) {
|
||||
[weakSelf signalAccountWasSelected:signalAccount];
|
||||
}
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
__block TSThread *thread = nil;
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
thread = [TSContactThread getOrCreateThreadWithContactSessionID:signalAccount.recipientId transaction:transaction];
|
||||
}];
|
||||
OWSAssertDebug(thread);
|
||||
|
||||
[self.selectThreadViewDelegate threadWasSelected:thread];
|
||||
}
|
||||
|
||||
#pragma mark - Events
|
||||
|
||||
- (void)dismissPressed:(id)sender
|
||||
{
|
||||
[self.searchBar resignFirstResponder];
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - OWSTableViewControllerDelegate
|
||||
|
||||
- (void)tableViewWillBeginDragging
|
||||
{
|
||||
[self.searchBar resignFirstResponder];
|
||||
}
|
||||
|
||||
#pragma mark - ThreadViewHelperDelegate
|
||||
|
||||
- (void)threadListDidChange
|
||||
{
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - ContactsViewHelperDelegate
|
||||
|
||||
- (void)contactsViewHelperDidUpdateContacts
|
||||
{
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (BOOL)shouldHideLocalNumber
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,21 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/SelectThreadViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class SignalAttachment;
|
||||
|
||||
@protocol ShareViewDelegate;
|
||||
|
||||
@interface SharingThreadPickerViewController : SelectThreadViewController
|
||||
|
||||
@property (nonatomic) NSArray<SignalAttachment *> *attachments;
|
||||
|
||||
- (instancetype)initWithShareViewDelegate:(id<ShareViewDelegate>)shareViewDelegate;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,458 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SharingThreadPickerViewController.h"
|
||||
#import "SignalApp.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <PromiseKit/PromiseKit.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||
#import <SignalUtilitiesKit/OWSError.h>
|
||||
#import <SessionMessagingKit/SessionMessagingKit.h>
|
||||
#import <SessionUIKit/SessionUIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef void (^SendCompletionBlock)(NSError *_Nullable, TSOutgoingMessage *);
|
||||
typedef void (^SendMessageBlock)(SendCompletionBlock completion);
|
||||
|
||||
@interface SharingThreadPickerViewController () <SelectThreadViewControllerDelegate,
|
||||
AttachmentApprovalViewControllerDelegate,
|
||||
MessageApprovalViewControllerDelegate>
|
||||
|
||||
@property (nonatomic, readonly) OWSMessageSender *messageSender;
|
||||
@property (nonatomic) TSThread *thread;
|
||||
@property (nonatomic, readonly, weak) id<ShareViewDelegate> shareViewDelegate;
|
||||
@property (atomic, nullable) TSOutgoingMessage *outgoingMessage;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation SharingThreadPickerViewController
|
||||
|
||||
- (instancetype)initWithShareViewDelegate:(id<ShareViewDelegate>)shareViewDelegate
|
||||
{
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_shareViewDelegate = shareViewDelegate;
|
||||
self.selectThreadViewDelegate = self;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Dependencies
|
||||
|
||||
- (YapDatabaseConnection *)dbReadWriteConnection
|
||||
{
|
||||
return OWSPrimaryStorage.sharedManager.dbReadWriteConnection;
|
||||
}
|
||||
|
||||
- (YapDatabaseConnection *)dbReadConnection
|
||||
{
|
||||
return OWSPrimaryStorage.sharedManager.dbReadConnection;
|
||||
}
|
||||
|
||||
#pragma mark - UIViewController overrides
|
||||
|
||||
- (void)loadView
|
||||
{
|
||||
[super loadView];
|
||||
|
||||
self.title = NSLocalizedString(@"SHARE_EXTENSION_VIEW_TITLE", @"Title for the 'share extension' view.");
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
// Loki: Customize title
|
||||
UILabel *titleLabel = [UILabel new];
|
||||
titleLabel.text = NSLocalizedString(@"Share", @"");
|
||||
titleLabel.textColor = LKColors.text;
|
||||
titleLabel.font = [UIFont boldSystemFontOfSize:25];
|
||||
self.navigationItem.titleView = titleLabel;
|
||||
}
|
||||
|
||||
- (BOOL)canSelectBlockedContact
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (nullable UIView *)createHeaderWithSearchBar:(UISearchBar *)searchBar
|
||||
{
|
||||
OWSAssertDebug(searchBar);
|
||||
|
||||
const CGFloat contentVMargin = 0;
|
||||
|
||||
UIView *header = [UIView new];
|
||||
header.backgroundColor = LKColors.navigationBarBackground;
|
||||
|
||||
UIButton *cancelShareButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[header addSubview:cancelShareButton];
|
||||
|
||||
[cancelShareButton setTitle:[CommonStrings cancelButton] forState:UIControlStateNormal];
|
||||
cancelShareButton.userInteractionEnabled = YES;
|
||||
|
||||
[cancelShareButton autoPinEdgeToSuperviewMargin:ALEdgeLeading];
|
||||
[cancelShareButton autoPinEdgeToSuperviewMargin:ALEdgeBottom];
|
||||
[cancelShareButton setCompressionResistanceHigh];
|
||||
[cancelShareButton setContentHuggingHigh];
|
||||
|
||||
[cancelShareButton addTarget:self
|
||||
action:@selector(didTapCancelShareButton)
|
||||
forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
[header addSubview:searchBar];
|
||||
[searchBar autoPinEdge:ALEdgeLeading toEdge:ALEdgeTrailing ofView:cancelShareButton withOffset:6];
|
||||
[searchBar autoPinEdgeToSuperviewEdge:ALEdgeTrailing];
|
||||
[searchBar autoPinEdgeToSuperviewEdge:ALEdgeTop];
|
||||
[searchBar autoPinEdgeToSuperviewEdge:ALEdgeBottom];
|
||||
|
||||
UIView *borderView = [UIView new];
|
||||
[header addSubview:borderView];
|
||||
|
||||
borderView.backgroundColor = [UIColor colorWithRGBHex:0xbbbbbb];
|
||||
[borderView autoSetDimension:ALDimensionHeight toSize:0.5];
|
||||
[borderView autoPinWidthToSuperview];
|
||||
[borderView autoPinEdgeToSuperviewEdge:ALEdgeBottom];
|
||||
|
||||
// UITableViewController.tableHeaderView must have its height set.
|
||||
header.frame = CGRectMake(0, 0, 0, (contentVMargin * 2 + searchBar.frame.size.height));
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
#pragma mark - SelectThreadViewControllerDelegate
|
||||
|
||||
- (nullable NSString *)convertAttachmentToMessageTextIfPossible
|
||||
{
|
||||
if (self.attachments.count > 1) {
|
||||
return nil;
|
||||
}
|
||||
OWSAssertDebug(self.attachments.count == 1);
|
||||
SignalAttachment *attachment = self.attachments.firstObject;
|
||||
if (!attachment.isConvertibleToTextMessage) {
|
||||
return nil;
|
||||
}
|
||||
if (attachment.dataLength >= kOversizeTextMessageSizeThreshold) {
|
||||
return nil;
|
||||
}
|
||||
NSData *data = attachment.data;
|
||||
OWSAssertDebug(data.length < kOversizeTextMessageSizeThreshold);
|
||||
NSString *_Nullable messageText = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
OWSLogVerbose(@"messageTextForAttachment: %@", messageText);
|
||||
return [messageText filterStringForDisplay];
|
||||
}
|
||||
|
||||
- (void)threadWasSelected:(TSThread *)thread
|
||||
{
|
||||
OWSAssertDebug(self.attachments.count > 0);
|
||||
OWSAssertDebug(thread);
|
||||
|
||||
self.thread = thread;
|
||||
|
||||
if ([self tryToShareAsMessageText]) {
|
||||
return;
|
||||
}
|
||||
|
||||
OWSNavigationController *approvalModal =
|
||||
[AttachmentApprovalViewController wrappedInNavControllerWithAttachments:self.attachments approvalDelegate:self];
|
||||
[self presentViewController:approvalModal animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (BOOL)tryToShareAsMessageText
|
||||
{
|
||||
OWSAssertDebug(self.attachments.count > 0);
|
||||
|
||||
NSString *_Nullable messageText = [self convertAttachmentToMessageTextIfPossible];
|
||||
if (!messageText) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
MessageApprovalViewController *approvalVC =
|
||||
[[MessageApprovalViewController alloc] initWithMessageText:messageText
|
||||
thread:self.thread
|
||||
delegate:self];
|
||||
|
||||
[self.navigationController pushViewController:approvalVC animated:YES];
|
||||
return YES;
|
||||
}
|
||||
|
||||
// override
|
||||
- (void)dismissPressed:(id)sender
|
||||
{
|
||||
OWSLogDebug(@"tapped dismiss share button");
|
||||
[self cancelShareExperience];
|
||||
}
|
||||
|
||||
- (void)didTapCancelShareButton
|
||||
{
|
||||
OWSLogDebug(@"tapped cancel share button");
|
||||
[self cancelShareExperience];
|
||||
}
|
||||
|
||||
- (void)cancelShareExperience
|
||||
{
|
||||
[self.shareViewDelegate shareViewWasCancelled];
|
||||
}
|
||||
|
||||
#pragma mark - AttachmentApprovalViewControllerDelegate
|
||||
|
||||
- (void)attachmentApproval:(AttachmentApprovalViewController *_Nonnull)attachmentApproval
|
||||
didApproveAttachments:(NSArray<SignalAttachment *> *_Nonnull)attachments
|
||||
messageText:(NSString *_Nullable)messageText
|
||||
{
|
||||
[self tryToSendMessageWithBlock:^(SendCompletionBlock sendCompletion) {
|
||||
SNVisibleMessage *message = [SNVisibleMessage new];
|
||||
message.sentTimestamp = [NSDate millisecondTimestamp];
|
||||
message.text = messageText;
|
||||
TSOutgoingMessage *tsMessage = [TSOutgoingMessage from:message associatedWith:self.thread];
|
||||
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[tsMessage saveWithTransaction:transaction];
|
||||
}];
|
||||
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[SNMessageSender sendNonDurably:message withAttachments:attachments inThread:self.thread usingTransaction:transaction]
|
||||
.then(^(id object) {
|
||||
sendCompletion(nil, tsMessage);
|
||||
}).catch(^(NSError *error) {
|
||||
sendCompletion(error, tsMessage);
|
||||
});
|
||||
}];
|
||||
|
||||
// This is necessary to show progress
|
||||
self.outgoingMessage = tsMessage;
|
||||
} fromViewController:attachmentApproval];
|
||||
}
|
||||
|
||||
- (void)attachmentApprovalDidCancel:(AttachmentApprovalViewController *)attachmentApproval
|
||||
{
|
||||
[self cancelShareExperience];
|
||||
}
|
||||
|
||||
- (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval
|
||||
didChangeMessageText:(nullable NSString *)newMessageText
|
||||
{
|
||||
// no-op
|
||||
}
|
||||
|
||||
#pragma mark - MessageApprovalViewControllerDelegate
|
||||
|
||||
- (void)messageApproval:(MessageApprovalViewController *)approvalViewController
|
||||
didApproveMessage:(NSString *)messageText
|
||||
{
|
||||
[self tryToSendMessageWithBlock:^(SendCompletionBlock sendCompletion) {
|
||||
SNVisibleMessage *message = [SNVisibleMessage new];
|
||||
message.sentTimestamp = [NSDate millisecondTimestamp];
|
||||
message.text = messageText;
|
||||
TSOutgoingMessage *tsMessage = [TSOutgoingMessage from:message associatedWith:self.thread];
|
||||
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[tsMessage saveWithTransaction:transaction];
|
||||
}];
|
||||
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[SNMessageSender sendNonDurably:message withAttachments:@[] inThread:self.thread usingTransaction:transaction]
|
||||
.then(^(id object) {
|
||||
sendCompletion(nil, tsMessage);
|
||||
}).catch(^(NSError *error) {
|
||||
sendCompletion(error, tsMessage);
|
||||
});
|
||||
}];
|
||||
|
||||
// This is necessary to show progress
|
||||
self.outgoingMessage = tsMessage;
|
||||
} fromViewController:approvalViewController];
|
||||
}
|
||||
|
||||
- (void)messageApprovalDidCancel:(MessageApprovalViewController *)approvalViewController
|
||||
{
|
||||
[self cancelShareExperience];
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
- (void)tryToSendMessageWithBlock:(SendMessageBlock)sendMessageBlock
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
{
|
||||
|
||||
NSString *progressTitle = NSLocalizedString(@"SHARE_EXTENSION_SENDING_IN_PROGRESS_TITLE", @"Alert title");
|
||||
UIAlertController *progressAlert = [UIAlertController alertControllerWithTitle:progressTitle
|
||||
message:nil
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
UIAlertAction *progressCancelAction = [UIAlertAction actionWithTitle:[CommonStrings cancelButton]
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[self.shareViewDelegate shareViewWasCancelled];
|
||||
}];
|
||||
[progressAlert addAction:progressCancelAction];
|
||||
|
||||
SendCompletionBlock sendCompletion = ^(NSError *_Nullable error, TSOutgoingMessage *message) {
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (error) {
|
||||
[fromViewController
|
||||
dismissViewControllerAnimated:YES
|
||||
completion:^{
|
||||
OWSLogInfo(@"Sending message failed with error: %@", error);
|
||||
[self showSendFailureAlertWithError:error
|
||||
message:message
|
||||
fromViewController:fromViewController];
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
OWSLogInfo(@"Sending message succeeded.");
|
||||
[self.shareViewDelegate shareViewWasCompleted];
|
||||
});
|
||||
};
|
||||
|
||||
[fromViewController presentAlert:progressAlert
|
||||
completion:^{
|
||||
sendMessageBlock(sendCompletion);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)showSendFailureAlertWithError:(NSError *)error
|
||||
message:(TSOutgoingMessage *)message
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(error);
|
||||
OWSAssertDebug(message);
|
||||
OWSAssertDebug(fromViewController);
|
||||
|
||||
NSString *failureTitle = NSLocalizedString(@"SHARE_EXTENSION_SENDING_FAILURE_TITLE", @"Alert title");
|
||||
|
||||
if ([error.domain isEqual:OWSSignalServiceKitErrorDomain] && error.code == OWSErrorCodeUntrustedIdentity) {
|
||||
NSString *_Nullable untrustedRecipientId = error.userInfo[OWSErrorRecipientIdentifierKey];
|
||||
|
||||
NSString *failureFormat = NSLocalizedString(@"SHARE_EXTENSION_FAILED_SENDING_BECAUSE_UNTRUSTED_IDENTITY_FORMAT",
|
||||
@"alert body when sharing file failed because of untrusted/changed identity keys");
|
||||
|
||||
SNContactContext context = [SNContact contextForThread:self.thread];
|
||||
NSString *displayName = [[LKStorage.shared getContactWithSessionID:untrustedRecipientId] displayNameFor:context] ?: untrustedRecipientId;
|
||||
NSString *failureMessage = [NSString stringWithFormat:failureFormat, displayName];
|
||||
|
||||
UIAlertController *failureAlert = [UIAlertController alertControllerWithTitle:failureTitle
|
||||
message:failureMessage
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
UIAlertAction *failureCancelAction = [UIAlertAction actionWithTitle:[CommonStrings cancelButton]
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[self.shareViewDelegate shareViewWasCancelled];
|
||||
}];
|
||||
[failureAlert addAction:failureCancelAction];
|
||||
|
||||
if (untrustedRecipientId.length > 0) {
|
||||
UIAlertAction *confirmAction =
|
||||
[UIAlertAction actionWithTitle:[SafetyNumberStrings confirmSendButton]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[self confirmIdentityAndResendMessage:message
|
||||
recipientId:untrustedRecipientId
|
||||
fromViewController:fromViewController];
|
||||
}];
|
||||
|
||||
[failureAlert addAction:confirmAction];
|
||||
} else {
|
||||
// This shouldn't happen, but if it does we won't offer the user the ability to confirm.
|
||||
// They may have to return to the main app to accept the identity change.
|
||||
OWSFailDebug(@"Untrusted recipient error is missing recipient id.");
|
||||
}
|
||||
|
||||
[fromViewController presentAlert:failureAlert];
|
||||
} else {
|
||||
// Non-identity failure, e.g. network offline, rate limit
|
||||
|
||||
UIAlertController *failureAlert = [UIAlertController alertControllerWithTitle:failureTitle
|
||||
message:error.localizedDescription
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
UIAlertAction *failureCancelAction = [UIAlertAction actionWithTitle:[CommonStrings cancelButton]
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[self.shareViewDelegate shareViewWasCancelled];
|
||||
}];
|
||||
[failureAlert addAction:failureCancelAction];
|
||||
|
||||
UIAlertAction *retryAction =
|
||||
[UIAlertAction actionWithTitle:[CommonStrings retryButton]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction *action) {
|
||||
[self resendMessage:message fromViewController:fromViewController];
|
||||
}];
|
||||
|
||||
[failureAlert addAction:retryAction];
|
||||
[fromViewController presentAlert:failureAlert];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)confirmIdentityAndResendMessage:(TSOutgoingMessage *)message
|
||||
recipientId:(NSString *)recipientId
|
||||
fromViewController:(UIViewController *)fromViewController
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(message);
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
OWSAssertDebug(fromViewController);
|
||||
|
||||
OWSLogDebug(@"Confirming identity for recipient: %@", recipientId);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||
[self resendMessage:message fromViewController:fromViewController];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)resendMessage:(TSOutgoingMessage *)tsMessage fromViewController:(UIViewController *)fromViewController
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(tsMessage);
|
||||
OWSAssertDebug(fromViewController);
|
||||
|
||||
NSString *progressTitle = NSLocalizedString(@"SHARE_EXTENSION_SENDING_IN_PROGRESS_TITLE", @"Alert title");
|
||||
UIAlertController *progressAlert = [UIAlertController alertControllerWithTitle:progressTitle
|
||||
message:nil
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
UIAlertAction *progressCancelAction = [UIAlertAction actionWithTitle:[CommonStrings cancelButton]
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction *_Nonnull action) {
|
||||
[self.shareViewDelegate shareViewWasCancelled];
|
||||
}];
|
||||
[progressAlert addAction:progressCancelAction];
|
||||
|
||||
[fromViewController
|
||||
presentAlert:progressAlert
|
||||
completion:^{
|
||||
SNVisibleMessage *message = [SNVisibleMessage from:tsMessage];
|
||||
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
NSMutableArray<TSAttachmentStream *> *attachments = @[].mutableCopy;
|
||||
for (NSString *attachmentID in tsMessage.attachmentIds) {
|
||||
TSAttachmentStream *stream = [TSAttachmentStream fetchObjectWithUniqueID:attachmentID transaction:transaction];
|
||||
if (![stream isKindOfClass:TSAttachmentStream.class]) { continue; }
|
||||
[attachments addObject:stream];
|
||||
}
|
||||
[SNMessageSender prep:attachments forMessage:message usingTransaction: transaction];
|
||||
[SNMessageSender sendNonDurably:message withAttachmentIDs:tsMessage.attachmentIds inThread:self.thread usingTransaction:transaction]
|
||||
.thenOn(dispatch_get_main_queue(), ^() {
|
||||
[self.shareViewDelegate shareViewWasCompleted];
|
||||
})
|
||||
.catchOn(dispatch_get_main_queue(), ^(NSError *error) {
|
||||
[fromViewController dismissViewControllerAnimated:YES completion:^{
|
||||
[self showSendFailureAlertWithError:error message:tsMessage fromViewController:fromViewController];
|
||||
}];
|
||||
});
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
Loading…
Reference in New Issue