Merge MatrixKit develop with commit hash: b85b736313bec0592bd1cabc68035d97f5331137

This commit is contained in:
SBiOSoftWhare
2021-12-03 11:47:24 +01:00
parent 8d15fe55a2
commit e7d4cd7707
475 changed files with 87437 additions and 0 deletions
@@ -0,0 +1,435 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <MatrixSDK/MatrixSDK.h>
@class MXKAccount;
/**
Posted when account user information (display name, picture, presence) has been updated.
The notification object is the matrix user id of the account.
*/
extern NSString *const kMXKAccountUserInfoDidChangeNotification;
/**
Posted when the activity of the Apple Push Notification Service has been changed.
The notification object is the matrix user id of the account.
*/
extern NSString *const kMXKAccountAPNSActivityDidChangeNotification;
/**
Posted when the activity of the Push notification based on PushKit has been changed.
The notification object is the matrix user id of the account.
*/
extern NSString *const kMXKAccountPushKitActivityDidChangeNotification;
/**
MXKAccount error domain
*/
extern NSString *const kMXKAccountErrorDomain;
/**
Block called when a certificate change is observed during authentication challenge from a server.
@param mxAccount the account concerned by this certificate change.
@param certificate the server certificate to evaluate.
@return YES to accept/trust this certificate, NO to cancel/ignore it.
*/
typedef BOOL (^MXKAccountOnCertificateChange)(MXKAccount *mxAccount, NSData *certificate);
/**
`MXKAccount` object contains the credentials of a logged matrix user. It is used to handle matrix
session and presence for this user.
*/
@interface MXKAccount : NSObject <NSCoding>
/**
The account's credentials: homeserver, access token, user id.
*/
@property (nonatomic, readonly) MXCredentials *mxCredentials;
/**
The identity server URL.
*/
@property (nonatomic) NSString *identityServerURL;
/**
The antivirus server URL, if any (nil by default).
Set a non-null url to configure the antivirus scanner use.
*/
@property (nonatomic) NSString *antivirusServerURL;
/**
The Push Gateway URL used to send event notifications to (nil by default).
This URL should be over HTTPS and never over HTTP.
*/
@property (nonatomic) NSString *pushGatewayURL;
/**
The matrix REST client used to make matrix API requests.
*/
@property (nonatomic, readonly) MXRestClient *mxRestClient;
/**
The matrix session opened with the account (nil by default).
*/
@property (nonatomic, readonly) MXSession *mxSession;
/**
The account user's display name (nil by default, available if matrix session `mxSession` is opened).
The notification `kMXKAccountUserInfoDidChangeNotification` is posted in case of change of this property.
*/
@property (nonatomic, readonly) NSString *userDisplayName;
/**
The account user's avatar url (nil by default, available if matrix session `mxSession` is opened).
The notification `kMXKAccountUserInfoDidChangeNotification` is posted in case of change of this property.
*/
@property (nonatomic, readonly) NSString *userAvatarUrl;
/**
The account display name based on user id and user displayname (if any).
*/
@property (nonatomic, readonly) NSString *fullDisplayName;
/**
The 3PIDs linked to this account.
[self load3PIDs] must be called to update the property.
*/
@property (nonatomic, readonly) NSArray<MXThirdPartyIdentifier *> *threePIDs;
/**
The email addresses linked to this account.
This is a subset of self.threePIDs.
*/
@property (nonatomic, readonly) NSArray<NSString *> *linkedEmails;
/**
The phone numbers linked to this account.
This is a subset of self.threePIDs.
*/
@property (nonatomic, readonly) NSArray<NSString *> *linkedPhoneNumbers;
/**
The account user's device.
[self loadDeviceInformation] must be called to update the property.
*/
@property (nonatomic, readonly) MXDevice *device;
/**
The account user's presence (`MXPresenceUnknown` by default, available if matrix session `mxSession` is opened).
The notification `kMXKAccountUserInfoDidChangeNotification` is posted in case of change of this property.
*/
@property (nonatomic, readonly) MXPresence userPresence;
/**
The account user's tint color: a unique color fixed by the user id. This tint color may be used to highlight
rooms which belong to this account's user.
*/
@property (nonatomic, readonly) UIColor *userTintColor;
/**
The Apple Push Notification Service activity for this account. YES when APNS is turned on (locally available and synced with server).
*/
@property (nonatomic, readonly) BOOL pushNotificationServiceIsActive;
/**
Transient information storage.
*/
@property (nonatomic, strong, readonly) NSMutableDictionary<NSString *, id<NSCoding>> *others;
/**
Enable Push notification based on Apple Push Notification Service (APNS).
This method creates or removes a pusher on the homeserver.
@param enable YES to enable it.
@param success A block object called when the operation succeeds.
@param failure A block object called when the operation fails.
*/
- (void)enablePushNotifications:(BOOL)enable
success:(void (^)(void))success
failure:(void (^)(NSError *))failure;
/**
Flag to indicate that an APNS pusher has been set on the homeserver for this device.
*/
@property (nonatomic, readonly) BOOL hasPusherForPushNotifications;
/**
The Push notification activity (based on PushKit) for this account.
YES when Push is turned on (locally available and enabled homeserver side).
*/
@property (nonatomic, readonly) BOOL isPushKitNotificationActive;
/**
Enable Push notification based on PushKit.
This method creates or removes a pusher on the homeserver.
@param enable YES to enable it.
@param success A block object called when the operation succeeds.
@param failure A block object called when the operation fails.
*/
- (void)enablePushKitNotifications:(BOOL)enable
success:(void (^)(void))success
failure:(void (^)(NSError *))failure;
/**
Flag to indicate that a PushKit pusher has been set on the homeserver for this device.
*/
@property (nonatomic, readonly) BOOL hasPusherForPushKitNotifications;
/**
Enable In-App notifications based on Remote notifications rules.
NO by default.
*/
@property (nonatomic) BOOL enableInAppNotifications;
/**
Disable the account without logging out (NO by default).
A matrix session is automatically opened for the account when this property is toggled from YES to NO.
The session is closed when this property is set to YES.
*/
@property (nonatomic,getter=isDisabled) BOOL disabled;
/**
Manage the online presence event.
The presence event must not be sent if the application is launched by a push notification.
*/
@property (nonatomic) BOOL hideUserPresence;
/**
Flag indicating if the end user has been warned about encryption and its limitations.
*/
@property (nonatomic,getter=isWarnedAboutEncryption) BOOL warnedAboutEncryption;
/**
Register the MXKAccountOnCertificateChange block that will be used to handle certificate change during account use.
This block is nil by default, any new certificate is ignored/untrusted (this will abort the connection to the server).
@param onCertificateChangeBlock the block that will be used to handle certificate change.
*/
+ (void)registerOnCertificateChangeBlock:(MXKAccountOnCertificateChange)onCertificateChangeBlock;
/**
Get the color code related to a specific presence.
@param presence a user presence
@return color defined for the provided presence (nil if no color is defined).
*/
+ (UIColor*)presenceColor:(MXPresence)presence;
/**
Init `MXKAccount` instance with credentials. No matrix session is opened by default.
@param credentials user's credentials
*/
- (instancetype)initWithCredentials:(MXCredentials*)credentials;
/**
Create a matrix session based on the provided store.
When store data is ready, the live stream is automatically launched by synchronising the session with the server.
In case of failure during server sync, the method is reiterated until the data is up-to-date with the server.
This loop is stopped if you call [MXCAccount closeSession:], it is suspended if you call [MXCAccount pauseInBackgroundTask].
@param store the store to use for the session.
*/
-(void)openSessionWithStore:(id<MXStore>)store;
/**
Close the matrix session.
@param clearStore set YES to delete all store data.
*/
- (void)closeSession:(BOOL)clearStore;
/**
Invalidate the access token, close the matrix session and delete all store data.
@note This method is equivalent to `logoutSendingServerRequest:completion:` with `sendLogoutServerRequest` parameter to YES
@param completion the block to execute at the end of the operation (independently if it succeeded or not).
*/
- (void)logout:(void (^)(void))completion;
/**
Invalidate the access token, close the matrix session and delete all store data.
@param sendLogoutServerRequest indicate to send logout request to homeserver.
@param completion the block to execute at the end of the operation (independently if it succeeded or not).
*/
- (void)logoutSendingServerRequest:(BOOL)sendLogoutServerRequest
completion:(void (^)(void))completion;
#pragma mark - Soft logout
/**
Flag to indicate if the account has been logged out by the homeserver admin.
*/
@property (nonatomic, readonly) BOOL isSoftLogout;
/**
Soft logout the account.
The matix session is stopped but the data is kept.
*/
- (void)softLogout;
/**
Hydrate the account using the credentials provided.
@param credentials the new credentials.
*/
- (void)hydrateWithCredentials:(MXCredentials*)credentials;
/**
Pause the current matrix session.
@warning: This matrix session is paused without using background task if no background mode handler
is set in the MXSDKOptions sharedInstance (see `backgroundModeHandler`).
*/
- (void)pauseInBackgroundTask;
/**
Perform a background sync by keeping the user offline.
@warning: This operation failed when no background mode handler is set in the
MXSDKOptions sharedInstance (see `backgroundModeHandler`).
@param timeout the timeout in milliseconds.
@param success A block object called when the operation succeeds.
@param failure A block object called when the operation fails.
*/
- (void)backgroundSync:(unsigned int)timeout success:(void (^)(void))success failure:(void (^)(NSError *))failure;
/**
Resume the current matrix session.
*/
- (void)resume;
/**
Close the potential matrix session and open a new one if the account is not disabled.
@param clearCache set YES to delete all store data.
*/
- (void)reload:(BOOL)clearCache;
/**
Set the display name of the account user.
@param displayname the new display name.
@param success A block object called when the operation succeeds.
@param failure A block object called when the operation fails.
*/
- (void)setUserDisplayName:(NSString*)displayname success:(void (^)(void))success failure:(void (^)(NSError *error))failure;
/**
Set the avatar url of the account user.
@param avatarUrl the new avatar url.
@param success A block object called when the operation succeeds.
@param failure A block object called when the operation fails.
*/
- (void)setUserAvatarUrl:(NSString*)avatarUrl success:(void (^)(void))success failure:(void (^)(NSError *error))failure;
/**
Update the account password.
@param oldPassword the old password.
@param newPassword the new password.
@param success A block object called when the operation succeeds.
@param failure A block object called when the operation fails.
*/
- (void)changePassword:(NSString*)oldPassword with:(NSString*)newPassword success:(void (^)(void))success failure:(void (^)(NSError *error))failure;
/**
Load the 3PIDs linked to this account.
This method must be called to refresh self.threePIDs and self.linkedEmails.
@param success A block object called when the operation succeeds.
@param failure A block object called when the operation fails.
*/
- (void)load3PIDs:(void (^)(void))success failure:(void (^)(NSError *error))failure;
/**
Load the current device information for this account.
This method must be called to refresh self.device.
@param success A block object called when the operation succeeds.
@param failure A block object called when the operation fails.
*/
- (void)loadDeviceInformation:(void (^)(void))success failure:(void (^)(NSError *error))failure;
#pragma mark - Push notification listeners
/**
Register a listener to push notifications for the account's session.
The listener will be called when a push rule matches a live event.
Note: only one listener is supported. Potential existing listener is removed.
You may use `[MXCAccount updateNotificationListenerForRoomId:]` to disable/enable all notifications from a specific room.
@param onNotification the block that will be called once a live event matches a push rule.
*/
- (void)listenToNotifications:(MXOnNotification)onNotification;
/**
Unregister the listener.
*/
- (void)removeNotificationListener;
/**
Update the listener to ignore or restore notifications from a specific room.
@param roomID the id of the concerned room.
@param isIgnored YES to disable notifications from the specified room. NO to restore them.
*/
- (void)updateNotificationListenerForRoomId:(NSString*)roomID ignore:(BOOL)isIgnored;
#pragma mark - Crypto
/**
Delete the device id.
Call this method when the current device id cannot be used anymore.
*/
- (void)resetDeviceId;
#pragma mark - Sync filter
/**
Check if the homeserver supports room members lazy loading.
@param completion the check result.
*/
- (void)supportLazyLoadOfRoomMembers:(void (^)(BOOL supportLazyLoadOfRoomMembers))completion;
/**
Call this method at an appropriate time to attempt dehydrating to a new backup device
*/
- (void)attemptDeviceDehydrationWithKeyData:(NSData *)keyData
success:(void (^)(void))success
failure:(void (^)(NSError *error))failure;
@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,218 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "MXKAccount.h"
/**
Posted when the user logged in with a matrix account.
The notification object is the new added account.
*/
extern NSString *const kMXKAccountManagerDidAddAccountNotification;
/**
Posted when an existing account is logged out.
The notification object is the removed account.
*/
extern NSString *const kMXKAccountManagerDidRemoveAccountNotification;
/**
Posted when an existing account is soft logged out.
The notification object is the account.
*/
extern NSString *const kMXKAccountManagerDidSoftlogoutAccountNotification;
/**
Used to identify the type of data when requesting MXKeyProvider
*/
extern NSString *const MXKAccountManagerDataType;
/**
`MXKAccountManager` manages a pool of `MXKAccount` instances.
*/
@interface MXKAccountManager : NSObject
/**
The class of store used to open matrix session for the accounts. This class must be conformed to MXStore protocol.
By default this class is MXFileStore.
*/
@property (nonatomic) Class storeClass;
/**
List of all available accounts (enabled and disabled).
*/
@property (nonatomic, readonly) NSArray<MXKAccount *> *accounts;
/**
List of active accounts (only enabled accounts)
*/
@property (nonatomic, readonly) NSArray<MXKAccount *> *activeAccounts;
/**
The device token used for Apple Push Notification Service registration.
*/
@property (nonatomic, copy) NSData *apnsDeviceToken;
/**
The APNS status: YES when app is registered for remote notif, and device token is known.
*/
@property (nonatomic) BOOL isAPNSAvailable;
/**
The device token used for Push notifications registration (PushKit support).
*/
@property (nonatomic, copy, readonly) NSData *pushDeviceToken;
/**
The current options of the Push notifications based on PushKit.
*/
@property (nonatomic, copy, readonly) NSDictionary *pushOptions;
/**
Set the push token and the potential push options.
For example, for clients that want to go & fetch the body of the event themselves anyway,
the key-value `format: event_id_only` may be used in `pushOptions` dictionary to tell the
HTTP pusher to send just the event_id of the event it's notifying about, the room id and
the notification counts.
@param pushDeviceToken the push token.
@param pushOptions dictionary of the push options (may be nil).
*/
- (void)setPushDeviceToken:(NSData *)pushDeviceToken withPushOptions:(NSDictionary *)pushOptions;
/**
The PushKit status: YES when app is registered for push notif, and push token is known.
*/
@property (nonatomic) BOOL isPushAvailable;
@property (nonatomic, readonly) MXDehydrationService *dehydrationService;
/**
Retrieve the MXKAccounts manager.
@return the MXKAccounts manager.
*/
+ (MXKAccountManager *)sharedManager;
/**
Check for each enabled account if a matrix session is already opened.
Open a matrix session for each enabled account which doesn't have a session.
The developper must set 'storeClass' before the first call of this method
if the default class is not suitable.
*/
- (void)prepareSessionForActiveAccounts;
/**
Save a snapshot of the current accounts.
*/
- (void)saveAccounts;
/**
Add an account and save the new account list. Optionally a matrix session may be opened for the provided account.
@param account a matrix account.
@param openSession YES to open a matrix session (this value is ignored if the account is disabled).
*/
- (void)addAccount:(MXKAccount *)account andOpenSession:(BOOL)openSession;
/**
Remove the provided account and save the new account list. This method is used in case of logout.
@note equivalent to `removeAccount:sendLogoutRequest:completion:` method with `sendLogoutRequest` parameter to YES
@param account a matrix account.
@param completion the block to execute at the end of the operation.
*/
- (void)removeAccount:(MXKAccount *)account completion:(void (^)(void))completion;
/**
Remove the provided account and save the new account list. This method is used in case of logout or account deactivation.
@param account a matrix account.
@param sendLogoutRequest Indicate whether send logout request to homeserver.
@param completion the block to execute at the end of the operation.
*/
- (void)removeAccount:(MXKAccount*)account
sendLogoutRequest:(BOOL)sendLogoutRequest
completion:(void (^)(void))completion;
/**
Log out and remove all the existing accounts
@param completion the block to execute at the end of the operation.
*/
- (void)logoutWithCompletion:(void (^)(void))completion;
/**
Soft logout an account.
@param account a matrix account.
*/
- (void)softLogout:(MXKAccount*)account;
/**
Hydrate an existing account by using the credentials provided.
This updates account credentials and restarts the account session
If the credentials belong to a different user from the account already stored,
the old account will be cleared automatically.
@param account a matrix account.
@param credentials the new credentials.
*/
- (void)hydrateAccount:(MXKAccount*)account withCredentials:(MXCredentials*)credentials;
/**
Retrieve the account for a user id.
@param userId the user id.
@return the user's account (nil if no account exist).
*/
- (MXKAccount *)accountForUserId:(NSString *)userId;
/**
Retrieve an account that knows the room with the passed id or alias.
Note: The method is not accurate as it returns the first account that matches.
@param roomIdOrAlias the room id or alias.
@return the user's account. Nil if no account matches.
*/
- (MXKAccount *)accountKnowingRoomWithRoomIdOrAlias:(NSString *)roomIdOrAlias;
/**
Retrieve an account that knows the user with the passed id.
Note: The method is not accurate as it returns the first account that matches.
@param userId the user id.
@return the user's account. Nil if no account matches.
*/
- (MXKAccount *)accountKnowingUserWithUserId:(NSString *)userId;
/**
Force the account manager to reload existing accounts from the local storage.
The account manager is supposed to handle itself the list of the accounts.
Call this method only when an account has been changed from an other application from the same group.
*/
- (void)forceReloadAccounts;
@end
@@ -0,0 +1,726 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKAccountManager.h"
#import "MXKAppSettings.h"
#import "MXKTools.h"
static NSString *const kMXKAccountsKeyOld = @"accounts";
static NSString *const kMXKAccountsKey = @"accountsV2";
NSString *const kMXKAccountManagerDidAddAccountNotification = @"kMXKAccountManagerDidAddAccountNotification";
NSString *const kMXKAccountManagerDidRemoveAccountNotification = @"kMXKAccountManagerDidRemoveAccountNotification";
NSString *const kMXKAccountManagerDidSoftlogoutAccountNotification = @"kMXKAccountManagerDidSoftlogoutAccountNotification";
NSString *const MXKAccountManagerDataType = @"org.matrix.kit.MXKAccountManagerDataType";
@interface MXKAccountManager()
{
/**
The list of all accounts (enabled and disabled). Each value is a `MXKAccount` instance.
*/
NSMutableArray<MXKAccount *> *mxAccounts;
}
@end
@implementation MXKAccountManager
+ (MXKAccountManager *)sharedManager
{
static MXKAccountManager *sharedAccountManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedAccountManager = [[super allocWithZone:NULL] init];
});
return sharedAccountManager;
}
- (instancetype)init
{
self = [super init];
if (self)
{
_storeClass = [MXFileStore class];
_dehydrationService = [MXDehydrationService new];
// Migrate old account file to new format
[self migrateAccounts];
// Load existing accounts from local storage
[self loadAccounts];
}
return self;
}
- (void)dealloc
{
mxAccounts = nil;
}
#pragma mark -
- (void)prepareSessionForActiveAccounts
{
for (MXKAccount *account in mxAccounts)
{
// Check whether the account is enabled. Open a new matrix session if none.
if (!account.isDisabled && !account.isSoftLogout && !account.mxSession)
{
MXLogDebug(@"[MXKAccountManager] openSession for %@ account", account.mxCredentials.userId);
id<MXStore> store = [[_storeClass alloc] init];
[account openSessionWithStore:store];
}
}
}
- (void)saveAccounts
{
NSDate *startDate = [NSDate date];
MXLogDebug(@"[MXKAccountManager] saveAccounts...");
NSMutableData *data = [NSMutableData data];
NSKeyedArchiver *encoder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[encoder encodeObject:mxAccounts forKey:@"mxAccounts"];
[encoder finishEncoding];
[data setData:[self encryptData:data]];
BOOL result = [data writeToFile:[self accountFile] atomically:YES];
MXLogDebug(@"[MXKAccountManager] saveAccounts. Done (result: %@) in %.0fms", @(result), [[NSDate date] timeIntervalSinceDate:startDate] * 1000);
}
- (void)addAccount:(MXKAccount *)account andOpenSession:(BOOL)openSession
{
MXLogDebug(@"[MXKAccountManager] login (%@)", account.mxCredentials.userId);
[mxAccounts addObject:account];
[self saveAccounts];
// Check conditions to open a matrix session
if (openSession && !account.disabled)
{
// Open a new matrix session by default
MXLogDebug(@"[MXKAccountManager] openSession for %@ account (device %@)", account.mxCredentials.userId, account.mxCredentials.deviceId);
id<MXStore> store = [[_storeClass alloc] init];
[account openSessionWithStore:store];
}
// Post notification
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountManagerDidAddAccountNotification object:account userInfo:nil];
}
- (void)removeAccount:(MXKAccount*)theAccount completion:(void (^)(void))completion
{
[self removeAccount:theAccount sendLogoutRequest:YES completion:completion];
}
- (void)removeAccount:(MXKAccount*)theAccount
sendLogoutRequest:(BOOL)sendLogoutRequest
completion:(void (^)(void))completion
{
MXLogDebug(@"[MXKAccountManager] logout (%@), send logout request to homeserver: %d", theAccount.mxCredentials.userId, sendLogoutRequest);
// Close session and clear associated store.
[theAccount logoutSendingServerRequest:sendLogoutRequest completion:^{
// Retrieve the corresponding account in the internal array
MXKAccount* removedAccount = nil;
for (MXKAccount *account in self->mxAccounts)
{
if ([account.mxCredentials.userId isEqualToString:theAccount.mxCredentials.userId])
{
removedAccount = account;
break;
}
}
if (removedAccount)
{
[self->mxAccounts removeObject:removedAccount];
[self saveAccounts];
// Post notification
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountManagerDidRemoveAccountNotification object:removedAccount userInfo:nil];
}
if (completion)
{
completion();
}
}];
}
- (void)logoutWithCompletion:(void (^)(void))completion
{
// Logout one by one the existing accounts
if (mxAccounts.count)
{
[self removeAccount:mxAccounts.lastObject completion:^{
// loop: logout the next existing account (if any)
[self logoutWithCompletion:completion];
}];
return;
}
NSUserDefaults *sharedUserDefaults = [MXKAppSettings standardAppSettings].sharedUserDefaults;
// Remove APNS device token
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"apnsDeviceToken"];
// Remove Push device token
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushDeviceToken"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushOptions"];
// Be sure that no account survive in local storage
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kMXKAccountsKey];
[sharedUserDefaults removeObjectForKey:kMXKAccountsKey];
[[NSFileManager defaultManager] removeItemAtPath:[self accountFile] error:nil];
if (completion)
{
completion();
}
}
- (void)softLogout:(MXKAccount*)account
{
[account softLogout];
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountManagerDidSoftlogoutAccountNotification
object:account
userInfo:nil];
}
- (void)hydrateAccount:(MXKAccount*)account withCredentials:(MXCredentials*)credentials
{
MXLogDebug(@"[MXKAccountManager] hydrateAccount: %@", account.mxCredentials.userId);
if ([account.mxCredentials.userId isEqualToString:credentials.userId])
{
// Restart the account
[account hydrateWithCredentials:credentials];
MXLogDebug(@"[MXKAccountManager] hydrateAccount: Open session");
id<MXStore> store = [[_storeClass alloc] init];
[account openSessionWithStore:store];
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountManagerDidAddAccountNotification
object:account
userInfo:nil];
}
else
{
MXLogDebug(@"[MXKAccountManager] hydrateAccount: Credentials given for another account: %@", credentials.userId);
// Logout the old account and create a new one with the new credentials
[self removeAccount:account sendLogoutRequest:YES completion:nil];
MXKAccount *newAccount = [[MXKAccount alloc] initWithCredentials:credentials];
[self addAccount:newAccount andOpenSession:YES];
}
}
- (MXKAccount *)accountForUserId:(NSString *)userId
{
for (MXKAccount *account in mxAccounts)
{
if ([account.mxCredentials.userId isEqualToString:userId])
{
return account;
}
}
return nil;
}
- (MXKAccount *)accountKnowingRoomWithRoomIdOrAlias:(NSString *)roomIdOrAlias
{
MXKAccount *theAccount = nil;
NSArray *activeAccounts = self.activeAccounts;
for (MXKAccount *account in activeAccounts)
{
if ([roomIdOrAlias hasPrefix:@"#"])
{
if ([account.mxSession roomWithAlias:roomIdOrAlias])
{
theAccount = account;
break;
}
}
else
{
if ([account.mxSession roomWithRoomId:roomIdOrAlias])
{
theAccount = account;
break;
}
}
}
return theAccount;
}
- (MXKAccount *)accountKnowingUserWithUserId:(NSString *)userId
{
MXKAccount *theAccount = nil;
NSArray *activeAccounts = self.activeAccounts;
for (MXKAccount *account in activeAccounts)
{
if ([account.mxSession userWithUserId:userId])
{
theAccount = account;
break;
}
}
return theAccount;
}
#pragma mark -
- (void)setStoreClass:(Class)storeClass
{
// Sanity check
NSAssert([storeClass conformsToProtocol:@protocol(MXStore)], @"MXKAccountManager only manages store class that conforms to MXStore protocol");
_storeClass = storeClass;
}
- (NSArray<MXKAccount *> *)accounts
{
return [mxAccounts copy];
}
- (NSArray<MXKAccount *> *)activeAccounts
{
NSMutableArray *activeAccounts = [NSMutableArray arrayWithCapacity:mxAccounts.count];
for (MXKAccount *account in mxAccounts)
{
if (!account.disabled && !account.isSoftLogout)
{
[activeAccounts addObject:account];
}
}
return activeAccounts;
}
- (NSData *)apnsDeviceToken
{
NSData *token = [[NSUserDefaults standardUserDefaults] objectForKey:@"apnsDeviceToken"];
if (!token.length)
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"apnsDeviceToken"];
token = nil;
}
MXLogDebug(@"[MXKAccountManager][Push] apnsDeviceToken: %@", [MXKTools logForPushToken:token]);
return token;
}
- (void)setApnsDeviceToken:(NSData *)apnsDeviceToken
{
MXLogDebug(@"[MXKAccountManager][Push] setApnsDeviceToken: %@", [MXKTools logForPushToken:apnsDeviceToken]);
NSData *oldToken = self.apnsDeviceToken;
if (!apnsDeviceToken.length)
{
MXLogDebug(@"[MXKAccountManager][Push] setApnsDeviceToken: reset APNS device token");
if (oldToken)
{
// turn off the Apns flag for all accounts if any
for (MXKAccount *account in mxAccounts)
{
[account enablePushNotifications:NO success:nil failure:nil];
}
}
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"apnsDeviceToken"];
}
else
{
NSArray *activeAccounts = self.activeAccounts;
if (!oldToken)
{
MXLogDebug(@"[MXKAccountManager][Push] setApnsDeviceToken: set APNS device token");
[[NSUserDefaults standardUserDefaults] setObject:apnsDeviceToken forKey:@"apnsDeviceToken"];
// turn on the Apns flag for all accounts, when the Apns registration succeeds for the first time
for (MXKAccount *account in activeAccounts)
{
[account enablePushNotifications:YES success:nil failure:nil];
}
}
else if (![oldToken isEqualToData:apnsDeviceToken])
{
MXLogDebug(@"[MXKAccountManager][Push] setApnsDeviceToken: update APNS device token");
NSMutableArray<MXKAccount*> *accountsWithAPNSPusher = [NSMutableArray new];
// Delete the pushers related to the old token
for (MXKAccount *account in activeAccounts)
{
if (account.hasPusherForPushNotifications)
{
[accountsWithAPNSPusher addObject:account];
}
[account enablePushNotifications:NO success:nil failure:nil];
}
// Update the token
[[NSUserDefaults standardUserDefaults] setObject:apnsDeviceToken forKey:@"apnsDeviceToken"];
// Refresh pushers with the new token.
for (MXKAccount *account in activeAccounts)
{
if ([accountsWithAPNSPusher containsObject:account])
{
MXLogDebug(@"[MXKAccountManager][Push] setApnsDeviceToken: Resync APNS for %@ account", account.mxCredentials.userId);
[account enablePushNotifications:YES success:nil failure:nil];
}
else
{
MXLogDebug(@"[MXKAccountManager][Push] setApnsDeviceToken: hasPusherForPushNotifications = NO for %@ account. Do not enable Push", account.mxCredentials.userId);
}
}
}
else
{
MXLogDebug(@"[MXKAccountManager][Push] setApnsDeviceToken: Same token. Nothing to do.");
}
}
}
- (BOOL)isAPNSAvailable
{
// [UIApplication isRegisteredForRemoteNotifications] tells whether your app can receive
// remote notifications or not. Receiving remote notifications does not guarantee it will
// display them to the user as they may have notifications set to deliver quietly.
BOOL isRemoteNotificationsAllowed = NO;
UIApplication *sharedApplication = [UIApplication performSelector:@selector(sharedApplication)];
if (sharedApplication)
{
isRemoteNotificationsAllowed = [sharedApplication isRegisteredForRemoteNotifications];
MXLogDebug(@"[MXKAccountManager][Push] isAPNSAvailable: The user %@ remote notification", (isRemoteNotificationsAllowed ? @"allowed" : @"denied"));
}
BOOL isAPNSAvailable = (isRemoteNotificationsAllowed && self.apnsDeviceToken);
MXLogDebug(@"[MXKAccountManager][Push] isAPNSAvailable: %@", @(isAPNSAvailable));
return isAPNSAvailable;
}
- (NSData *)pushDeviceToken
{
NSData *token = [[NSUserDefaults standardUserDefaults] objectForKey:@"pushDeviceToken"];
if (!token.length)
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushDeviceToken"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushOptions"];
token = nil;
}
MXLogDebug(@"[MXKAccountManager][Push] pushDeviceToken: %@", [MXKTools logForPushToken:token]);
return token;
}
- (NSDictionary *)pushOptions
{
NSDictionary *pushOptions = [[NSUserDefaults standardUserDefaults] objectForKey:@"pushOptions"];
MXLogDebug(@"[MXKAccountManager][Push] pushOptions: %@", pushOptions);
return pushOptions;
}
- (void)setPushDeviceToken:(NSData *)pushDeviceToken withPushOptions:(NSDictionary *)pushOptions
{
MXLogDebug(@"[MXKAccountManager][Push] setPushDeviceToken: %@ withPushOptions: %@", [MXKTools logForPushToken:pushDeviceToken], pushOptions);
NSData *oldToken = self.pushDeviceToken;
if (!pushDeviceToken.length)
{
MXLogDebug(@"[MXKAccountManager][Push] setPushDeviceToken: Reset Push device token");
if (oldToken)
{
// turn off the Push flag for all accounts if any
for (MXKAccount *account in mxAccounts)
{
[account enablePushKitNotifications:NO success:^{
// make sure pusher really removed before losing token.
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushDeviceToken"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushOptions"];
} failure:nil];
}
}
}
else
{
NSArray *activeAccounts = self.activeAccounts;
if (!oldToken)
{
MXLogDebug(@"[MXKAccountManager][Push] setPushDeviceToken: Set Push device token");
[[NSUserDefaults standardUserDefaults] setObject:pushDeviceToken forKey:@"pushDeviceToken"];
if (pushOptions)
{
[[NSUserDefaults standardUserDefaults] setObject:pushOptions forKey:@"pushOptions"];
}
else
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushOptions"];
}
// turn on the Push flag for all accounts
for (MXKAccount *account in activeAccounts)
{
[account enablePushKitNotifications:YES success:nil failure:nil];
}
}
else if (![oldToken isEqualToData:pushDeviceToken])
{
MXLogDebug(@"[MXKAccountManager][Push] setPushDeviceToken: Update Push device token");
NSMutableArray<MXKAccount*> *accountsWithPushKitPusher = [NSMutableArray new];
// Delete the pushers related to the old token
for (MXKAccount *account in activeAccounts)
{
if (account.hasPusherForPushKitNotifications)
{
[accountsWithPushKitPusher addObject:account];
}
[account enablePushKitNotifications:NO success:nil failure:nil];
}
// Update the token
[[NSUserDefaults standardUserDefaults] setObject:pushDeviceToken forKey:@"pushDeviceToken"];
if (pushOptions)
{
[[NSUserDefaults standardUserDefaults] setObject:pushOptions forKey:@"pushOptions"];
}
else
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushOptions"];
}
// Refresh pushers with the new token.
for (MXKAccount *account in activeAccounts)
{
if ([accountsWithPushKitPusher containsObject:account])
{
MXLogDebug(@"[MXKAccountManager][Push] setPushDeviceToken: Resync Push for %@ account", account.mxCredentials.userId);
[account enablePushKitNotifications:YES success:nil failure:nil];
}
else
{
MXLogDebug(@"[MXKAccountManager][Push] setPushDeviceToken: hasPusherForPushKitNotifications = NO for %@ account. Do not enable Push", account.mxCredentials.userId);
}
}
}
else
{
MXLogDebug(@"[MXKAccountManager][Push] setPushDeviceToken: Same token. Nothing to do.");
}
}
}
- (BOOL)isPushAvailable
{
// [UIApplication isRegisteredForRemoteNotifications] tells whether your app can receive
// remote notifications or not. Receiving remote notifications does not guarantee it will
// display them to the user as they may have notifications set to deliver quietly.
BOOL isRemoteNotificationsAllowed = NO;
UIApplication *sharedApplication = [UIApplication performSelector:@selector(sharedApplication)];
if (sharedApplication)
{
isRemoteNotificationsAllowed = [sharedApplication isRegisteredForRemoteNotifications];
MXLogDebug(@"[MXKAccountManager][Push] isPushAvailable: The user %@ remote notification", (isRemoteNotificationsAllowed ? @"allowed" : @"denied"));
}
BOOL isPushAvailable = (isRemoteNotificationsAllowed && self.pushDeviceToken);
MXLogDebug(@"[MXKAccountManager][Push] isPushAvailable: %@", @(isPushAvailable));
return isPushAvailable;
}
#pragma mark -
// Return the path of the file containing stored MXAccounts array
- (NSString*)accountFile
{
NSString *matrixKitCacheFolder = [MXKAppSettings cacheFolder];
return [matrixKitCacheFolder stringByAppendingPathComponent:kMXKAccountsKey];
}
- (void)loadAccounts
{
MXLogDebug(@"[MXKAccountManager] loadAccounts");
NSString *accountFile = [self accountFile];
if ([[NSFileManager defaultManager] fileExistsAtPath:accountFile])
{
NSDate *startDate = [NSDate date];
NSError *error = nil;
NSData* filecontent = [NSData dataWithContentsOfFile:accountFile options:(NSDataReadingMappedAlways | NSDataReadingUncached) error:&error];
if (!error)
{
// Decrypt data if encryption method is provided
NSData *unciphered = [self decryptData:filecontent];
NSKeyedUnarchiver *decoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:unciphered];
mxAccounts = [decoder decodeObjectForKey:@"mxAccounts"];
if (!mxAccounts && [[MXKeyProvider sharedInstance] isEncryptionAvailableForDataOfType:MXKAccountManagerDataType])
{
// This happens if the V2 file has not been encrypted -> read file content then save encrypted accounts
MXLogDebug(@"[MXKAccountManager] loadAccounts. Failed to read decrypted data: reading file data without encryption.");
decoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:filecontent];
mxAccounts = [decoder decodeObjectForKey:@"mxAccounts"];
if (mxAccounts)
{
MXLogDebug(@"[MXKAccountManager] loadAccounts. saving encrypted accounts");
[self saveAccounts];
}
}
}
MXLogDebug(@"[MXKAccountManager] loadAccounts. %tu accounts loaded in %.0fms", mxAccounts.count, [[NSDate date] timeIntervalSinceDate:startDate] * 1000);
}
else
{
// Migration of accountData from sharedUserDefaults to a file
NSUserDefaults *sharedDefaults = [MXKAppSettings standardAppSettings].sharedUserDefaults;
NSData *accountData = [sharedDefaults objectForKey:kMXKAccountsKey];
if (!accountData)
{
// Migration of accountData from [NSUserDefaults standardUserDefaults], the first location storage
accountData = [[NSUserDefaults standardUserDefaults] objectForKey:kMXKAccountsKey];
}
if (accountData)
{
mxAccounts = [NSMutableArray arrayWithArray:[NSKeyedUnarchiver unarchiveObjectWithData:accountData]];
[self saveAccounts];
MXLogDebug(@"[MXKAccountManager] loadAccounts: performed data migration");
// Now that data has been migrated, erase old location of accountData
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kMXKAccountsKey];
[sharedDefaults removeObjectForKey:kMXKAccountsKey];
}
}
if (!mxAccounts)
{
MXLogDebug(@"[MXKAccountManager] loadAccounts. No accounts");
mxAccounts = [NSMutableArray array];
}
}
- (void)forceReloadAccounts
{
MXLogDebug(@"[MXKAccountManager] Force reload existing accounts from local storage");
[self loadAccounts];
}
- (NSData*)encryptData:(NSData*)data
{
// Exceptions are not caught as the key is always needed if the KeyProviderDelegate
// is provided.
MXKeyData *keyData = [[MXKeyProvider sharedInstance] requestKeyForDataOfType:MXKAccountManagerDataType isMandatory:YES expectedKeyType:kAes];
if (keyData && [keyData isKindOfClass:[MXAesKeyData class]])
{
MXAesKeyData *aesKey = (MXAesKeyData *) keyData;
NSData *cipher = [MXAes encrypt:data aesKey:aesKey.key iv:aesKey.iv error:nil];
return cipher;
}
MXLogDebug(@"[MXKAccountManager] encryptData: no key method provided for encryption.");
return data;
}
- (NSData*)decryptData:(NSData*)data
{
// Exceptions are not cached as the key is always needed if the KeyProviderDelegate
// is provided.
MXKeyData *keyData = [[MXKeyProvider sharedInstance] requestKeyForDataOfType:MXKAccountManagerDataType isMandatory:YES expectedKeyType:kAes];
if (keyData && [keyData isKindOfClass:[MXAesKeyData class]])
{
MXAesKeyData *aesKey = (MXAesKeyData *) keyData;
NSData *decrypt = [MXAes decrypt:data aesKey:aesKey.key iv:aesKey.iv error:nil];
return decrypt;
}
MXLogDebug(@"[MXKAccountManager] decryptData: no key method provided for decryption.");
return data;
}
- (void)migrateAccounts
{
NSString *pathOld = [[MXKAppSettings cacheFolder] stringByAppendingPathComponent:kMXKAccountsKeyOld];
NSString *pathNew = [[MXKAppSettings cacheFolder] stringByAppendingPathComponent:kMXKAccountsKey];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:pathOld])
{
if (![fileManager fileExistsAtPath:pathNew])
{
MXLogDebug(@"[MXKAccountManager] migrateAccounts: reading account");
mxAccounts = [NSKeyedUnarchiver unarchiveObjectWithFile:pathOld];
MXLogDebug(@"[MXKAccountManager] migrateAccounts: writing to accountV2");
[self saveAccounts];
}
MXLogDebug(@"[MXKAccountManager] migrateAccounts: removing account");
[fileManager removeItemAtPath:pathOld error:nil];
}
}
@end
@@ -0,0 +1,170 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <UIKit/UIKit.h>
#import <AddressBook/AddressBook.h>
#import "MXKCellData.h"
#import "MXKEmail.h"
#import "MXKPhoneNumber.h"
/**
Posted when the contact thumbnail is updated.
The notification object is a contact Id.
*/
extern NSString *const kMXKContactThumbnailUpdateNotification;
extern NSString *const kMXKContactLocalContactPrefixId;
extern NSString *const kMXKContactMatrixContactPrefixId;
extern NSString *const kMXKContactDefaultContactPrefixId;
@interface MXKContact : MXKCellData <NSCoding>
/**
The unique identifier
*/
@property (nonatomic, readonly) NSString * contactID;
/**
The display name
*/
@property (nonatomic, readwrite) NSString *displayName;
/**
The sorting display name built by trimming the symbols [_!~`@#$%^&*-+();:={}[],.<>?\/"'] from the display name.
*/
@property (nonatomic) NSString* sortingDisplayName;
/**
The contact thumbnail. Default size: 256 X 256 pixels
*/
@property (nonatomic, copy, readonly) UIImage *thumbnail;
/**
YES if the contact does not exist in the contacts book
the contact has been created from a MXUser or MXRoomThirdPartyInvite
*/
@property (nonatomic) BOOL isMatrixContact;
/**
YES if the contact is coming from MXRoomThirdPartyInvite event (NO by default).
*/
@property (nonatomic) BOOL isThirdPartyInvite;
/**
The array of MXKPhoneNumber
*/
@property (nonatomic, readonly) NSArray *phoneNumbers;
/**
The array of MXKEmail
*/
@property (nonatomic, readonly) NSArray *emailAddresses;
/**
The array of matrix identifiers
*/
@property (nonatomic, readonly) NSArray* matrixIdentifiers;
/**
The matrix avatar url used (if any) to build the current thumbnail, nil by default.
*/
@property (nonatomic, readonly) NSString* matrixAvatarURL;
/**
Reset the current thumbnail if it is retrieved from a matrix url. May be used in case of the matrix avatar url change.
A new thumbnail will be automatically restored from the contact data.
*/
- (void)resetMatrixThumbnail;
/**
The contact ID from native phonebook record
*/
+ (NSString*)contactID:(ABRecordRef)record;
/**
Create a local contact from a device contact
@param record device contact id
@return MXKContact instance
*/
- (id)initLocalContactWithABRecord:(ABRecordRef)record;
/**
Create a matrix contact with the dedicated info
@param displayName the contact display name
@param matrixID the contact matrix id
@return MXKContact instance
*/
- (id)initMatrixContactWithDisplayName:(NSString*)displayName andMatrixID:(NSString*)matrixID;
/**
Create a matrix contact with the dedicated info
@param displayName the contact display name
@param matrixID the contact matrix id
@param matrixAvatarURL the matrix avatar url
@return MXKContact instance
*/
- (id)initMatrixContactWithDisplayName:(NSString*)displayName matrixID:(NSString*)matrixID andMatrixAvatarURL:(NSString*)matrixAvatarURL;
/**
Create a contact with the dedicated info
@param displayName the contact display name
@param emails an array of emails
@param phones an array of phone numbers
@param thumbnail the contact thumbnail
@return MXKContact instance
*/
- (id)initContactWithDisplayName:(NSString*)displayName
emails:(NSArray<MXKEmail*> *)emails
phoneNumbers:(NSArray<MXKPhoneNumber*> *)phones
andThumbnail:(UIImage *)thumbnail;
/**
The contact thumbnail with a prefered size.
If the thumbnail is already loaded, this method returns this one by ignoring prefered size.
The prefered size is used only if a server request is required.
@return thumbnail with a prefered size
*/
- (UIImage*)thumbnailWithPreferedSize:(CGSize)size;
/**
Tell whether a component of the contact's displayName, or one of his matrix id/email has the provided prefix.
@param prefix a non empty string.
@return YES when at least one matrix id, email or a component of the display name has this prefix.
*/
- (BOOL)hasPrefix:(NSString*)prefix;
/**
Check if the patterns can match with this contact
*/
- (BOOL)matchedWithPatterns:(NSArray*)patterns;
/**
The default ISO 3166-1 country code used to internationalize the contact phone numbers.
*/
@property (nonatomic) NSString *defaultCountryCode;
@end
@@ -0,0 +1,659 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKContact.h"
#import "MXKEmail.h"
#import "MXKPhoneNumber.h"
NSString *const kMXKContactThumbnailUpdateNotification = @"kMXKContactThumbnailUpdateNotification";
NSString *const kMXKContactLocalContactPrefixId = @"Local_";
NSString *const kMXKContactMatrixContactPrefixId = @"Matrix_";
NSString *const kMXKContactDefaultContactPrefixId = @"Default_";
@interface MXKContact()
{
UIImage* contactThumbnail;
UIImage* matrixThumbnail;
// The matrix id of the contact (used when the contact is not defined in the contacts book)
MXKContactField *matrixIdField;
}
@end
@implementation MXKContact
@synthesize isMatrixContact, isThirdPartyInvite;
+ (NSString*)contactID:(ABRecordRef)record
{
return [NSString stringWithFormat:@"%@%d", kMXKContactLocalContactPrefixId, ABRecordGetRecordID(record)];
}
- (id)init
{
self = [super init];
if (self)
{
matrixIdField = nil;
isMatrixContact = NO;
_matrixAvatarURL = nil;
isThirdPartyInvite = NO;
}
return self;
}
- (id)initLocalContactWithABRecord:(ABRecordRef)record
{
self = [self init];
if (self)
{
// compute a contact ID
_contactID = [MXKContact contactID:record];
// use the contact book display name
_displayName = (__bridge NSString*) ABRecordCopyCompositeName(record);
// avoid nil display name
// the display name is used to sort contacts
if (!_displayName)
{
_displayName = @"";
}
// extract the phone numbers and their related label
ABMultiValueRef multi = ABRecordCopyValue(record, kABPersonPhoneProperty);
CFIndex nCount = ABMultiValueGetCount(multi);
NSMutableArray* pns = [[NSMutableArray alloc] initWithCapacity:nCount];
for (int i = 0; i < nCount; i++)
{
CFTypeRef phoneRef = ABMultiValueCopyValueAtIndex(multi, i);
NSString *phoneVal = (__bridge NSString*)phoneRef;
// sanity check
if (0 != [phoneVal length])
{
CFStringRef lblRef = ABMultiValueCopyLabelAtIndex(multi, i);
CFStringRef localizedLblRef = nil;
NSString *lbl = @"";
if (lblRef != nil)
{
localizedLblRef = ABAddressBookCopyLocalizedLabel(lblRef);
if (localizedLblRef)
{
lbl = (__bridge NSString*)localizedLblRef;
}
else
{
lbl = (__bridge NSString*)lblRef;
}
}
else
{
localizedLblRef = ABAddressBookCopyLocalizedLabel(kABOtherLabel);
if (localizedLblRef)
{
lbl = (__bridge NSString*)localizedLblRef;
}
}
[pns addObject:[[MXKPhoneNumber alloc] initWithTextNumber:phoneVal type:lbl contactID:_contactID matrixID:nil]];
if (lblRef)
{
CFRelease(lblRef);
}
if (localizedLblRef)
{
CFRelease(localizedLblRef);
}
}
// release meory
if (phoneRef)
{
CFRelease(phoneRef);
}
}
CFRelease(multi);
_phoneNumbers = pns;
// extract the emails
multi = ABRecordCopyValue(record, kABPersonEmailProperty);
nCount = ABMultiValueGetCount(multi);
NSMutableArray *emails = [[NSMutableArray alloc] initWithCapacity:nCount];
for (int i = 0; i < nCount; i++)
{
CFTypeRef emailValRef = ABMultiValueCopyValueAtIndex(multi, i);
NSString *emailVal = (__bridge NSString*)emailValRef;
// sanity check
if ((nil != emailVal) && (0 != [emailVal length]))
{
CFStringRef lblRef = ABMultiValueCopyLabelAtIndex(multi, i);
CFStringRef localizedLblRef = nil;
NSString *lbl = @"";
if (lblRef != nil)
{
localizedLblRef = ABAddressBookCopyLocalizedLabel(lblRef);
if (localizedLblRef)
{
lbl = (__bridge NSString*)localizedLblRef;
}
else
{
lbl = (__bridge NSString*)lblRef;
}
}
else
{
localizedLblRef = ABAddressBookCopyLocalizedLabel(kABOtherLabel);
if (localizedLblRef)
{
lbl = (__bridge NSString*)localizedLblRef;
}
}
[emails addObject: [[MXKEmail alloc] initWithEmailAddress:emailVal type:lbl contactID:_contactID matrixID:nil]];
if (lblRef)
{
CFRelease(lblRef);
}
if (localizedLblRef)
{
CFRelease(localizedLblRef);
}
}
if (emailValRef)
{
CFRelease(emailValRef);
}
}
CFRelease(multi);
_emailAddresses = emails;
// thumbnail/picture
// check whether the contact has a picture
if (ABPersonHasImageData(record))
{
CFDataRef dataRef;
dataRef = ABPersonCopyImageDataWithFormat(record, kABPersonImageFormatThumbnail);
if (dataRef)
{
contactThumbnail = [UIImage imageWithData:(__bridge NSData*)dataRef];
CFRelease(dataRef);
}
}
}
return self;
}
- (id)initMatrixContactWithDisplayName:(NSString*)displayName andMatrixID:(NSString*)matrixID
{
self = [self init];
if (self)
{
_contactID = [NSString stringWithFormat:@"%@%@", kMXKContactMatrixContactPrefixId, [[NSUUID UUID] UUIDString]];
// Sanity check
if (matrixID.length)
{
// used when the contact is not defined in the contacts book
matrixIdField = [[MXKContactField alloc] initWithContactID:_contactID matrixID:matrixID];
isMatrixContact = YES;
}
// _displayName must not be nil
// it is used to sort the contacts
if (displayName)
{
_displayName = displayName;
}
else
{
_displayName = @"";
}
}
return self;
}
- (id)initMatrixContactWithDisplayName:(NSString*)displayName matrixID:(NSString*)matrixID andMatrixAvatarURL:(NSString*)matrixAvatarURL
{
self = [self initMatrixContactWithDisplayName:displayName andMatrixID:matrixID];
if (self)
{
matrixIdField.matrixAvatarURL = matrixAvatarURL;
}
return self;
}
- (id)initContactWithDisplayName:(NSString*)displayName
emails:(NSArray<MXKEmail*> *)emails
phoneNumbers:(NSArray<MXKPhoneNumber*> *)phones
andThumbnail:(UIImage *)thumbnail
{
self = [self init];
if (self)
{
_contactID = [NSString stringWithFormat:@"%@%@", kMXKContactDefaultContactPrefixId, [[NSUUID UUID] UUIDString]];
// _displayName must not be nil
// it is used to sort the contacts
if (displayName)
{
_displayName = displayName;
}
else
{
_displayName = @"";
}
_emailAddresses = emails;
_phoneNumbers = phones;
contactThumbnail = thumbnail;
}
return self;
}
#pragma mark -
- (NSString*)sortingDisplayName
{
if (!_sortingDisplayName)
{
// Sanity check - display name should not be nil here
if (self.displayName)
{
NSCharacterSet *specialCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"_!~`@#$%^&*-+();:={}[],.<>?\\/\"\'"];
_sortingDisplayName = [self.displayName stringByTrimmingCharactersInSet:specialCharacterSet];
}
else
{
return @"";
}
}
return _sortingDisplayName;
}
- (BOOL)hasPrefix:(NSString*)prefix
{
prefix = [prefix lowercaseString];
// Check first display name
if (_displayName.length)
{
NSString *lowercaseString = [_displayName lowercaseString];
if ([lowercaseString hasPrefix:prefix])
{
return YES;
}
NSArray *components = [lowercaseString componentsSeparatedByString:@" "];
for (NSString *component in components)
{
NSString *theComponent = [component stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if ([theComponent hasPrefix:prefix])
{
return YES;
}
}
}
// Check matrix identifiers
NSArray *identifiers = self.matrixIdentifiers;
NSString *idPrefix = prefix;
if (![prefix hasPrefix:@"@"])
{
idPrefix = [NSString stringWithFormat:@"@%@", prefix];
}
for (NSString* mxId in identifiers)
{
if ([[mxId lowercaseString] hasPrefix:idPrefix])
{
return YES;
}
}
// Check email
for (MXKEmail* email in _emailAddresses)
{
if ([email.emailAddress hasPrefix:prefix])
{
return YES;
}
}
// Check phones
for (MXKPhoneNumber* phone in _phoneNumbers)
{
if ([phone hasPrefix:prefix])
{
return YES;
}
}
return NO;
}
- (BOOL)matchedWithPatterns:(NSArray*)patterns
{
BOOL matched = NO;
if (patterns.count > 0)
{
matched = YES;
// test first display name
for (NSString* pattern in patterns)
{
if ([_displayName rangeOfString:pattern options:NSCaseInsensitiveSearch].location == NSNotFound)
{
matched = NO;
break;
}
}
NSArray *identifiers = self.matrixIdentifiers;
if (!matched && identifiers.count > 0)
{
for (NSString* mxId in identifiers)
{
// Consider only the first part of the matrix id (ignore homeserver name)
NSRange range = [mxId rangeOfString:@":"];
if (range.location != NSNotFound)
{
NSString *mxIdName = [mxId substringToIndex:range.location];
for (NSString* pattern in patterns)
{
if ([mxIdName rangeOfString:pattern options:NSCaseInsensitiveSearch].location != NSNotFound)
{
matched = YES;
break;
}
}
if (matched)
{
break;
}
}
}
}
if (!matched && _phoneNumbers.count > 0)
{
for (MXKPhoneNumber* phonenumber in _phoneNumbers)
{
if ([phonenumber matchedWithPatterns:patterns])
{
matched = YES;
break;
}
}
}
if (!matched && _emailAddresses.count > 0)
{
for (MXKEmail* email in _emailAddresses)
{
if ([email matchedWithPatterns:patterns])
{
matched = YES;
break;
}
}
}
}
else
{
// if there is no pattern to search, it should always matched
matched = YES;
}
return matched;
}
- (void)setDefaultCountryCode:(NSString *)defaultCountryCode
{
for (MXKPhoneNumber* phonenumber in _phoneNumbers)
{
phonenumber.defaultCountryCode = defaultCountryCode;
}
_defaultCountryCode = defaultCountryCode;
}
#pragma mark - getter/setter
- (NSArray*)matrixIdentifiers
{
NSMutableArray* identifiers = [[NSMutableArray alloc] init];
if (matrixIdField)
{
[identifiers addObject:matrixIdField.matrixID];
}
for (MXKEmail* email in _emailAddresses)
{
if (email.matrixID && ([identifiers indexOfObject:email.matrixID] == NSNotFound))
{
[identifiers addObject:email.matrixID];
}
}
for (MXKPhoneNumber* pn in _phoneNumbers)
{
if (pn.matrixID && ([identifiers indexOfObject:pn.matrixID] == NSNotFound))
{
[identifiers addObject:pn.matrixID];
}
}
return identifiers;
}
- (void)setDisplayName:(NSString *)displayName
{
// a display name must not be emptied
// it is used to sort the contacts
if (displayName.length == 0)
{
_displayName = _contactID;
}
else
{
_displayName = displayName;
}
}
- (void)resetMatrixThumbnail
{
matrixThumbnail = nil;
_matrixAvatarURL = nil;
// Reset the avatar in the contact fields too.
[matrixIdField resetMatrixAvatar];
for (MXKEmail* email in _emailAddresses)
{
[email resetMatrixAvatar];
}
}
- (UIImage*)thumbnailWithPreferedSize:(CGSize)size
{
// Consider first the local thumbnail if any.
if (contactThumbnail)
{
return contactThumbnail;
}
// Check whether a matrix thumbnail is already found.
if (matrixThumbnail)
{
return matrixThumbnail;
}
// Look for a thumbnail from the matrix identifiers
MXKContactField* firstField = matrixIdField;
if (firstField)
{
if (firstField.avatarImage)
{
matrixThumbnail = firstField.avatarImage;
_matrixAvatarURL = firstField.matrixAvatarURL;
return matrixThumbnail;
}
}
// try to replace the thumbnail by the matrix one
if (_emailAddresses.count > 0)
{
// list the linked email
// search if one email field has a dedicated thumbnail
for (MXKEmail* email in _emailAddresses)
{
if (email.avatarImage)
{
matrixThumbnail = email.avatarImage;
_matrixAvatarURL = email.matrixAvatarURL;
return matrixThumbnail;
}
else if (!firstField && email.matrixID)
{
firstField = email;
}
}
}
if (_phoneNumbers.count > 0)
{
// list the linked phones
// search if one phone field has a dedicated thumbnail
for (MXKPhoneNumber* phoneNb in _phoneNumbers)
{
if (phoneNb.avatarImage)
{
matrixThumbnail = phoneNb.avatarImage;
_matrixAvatarURL = phoneNb.matrixAvatarURL;
return matrixThumbnail;
}
else if (!firstField && phoneNb.matrixID)
{
firstField = phoneNb;
}
}
}
// if no thumbnail has been found
// try to load the first field one
if (firstField)
{
// should be retrieved by the cell info
[firstField loadAvatarWithSize:size];
}
return nil;
}
- (UIImage*)thumbnail
{
return [self thumbnailWithPreferedSize:CGSizeMake(256, 256)];
}
#pragma mark NSCoding
- (id)initWithCoder:(NSCoder *)coder
{
_contactID = [coder decodeObjectForKey:@"contactID"];
_displayName = [coder decodeObjectForKey:@"displayName"];
matrixIdField = [coder decodeObjectForKey:@"matrixIdField"];
_phoneNumbers = [coder decodeObjectForKey:@"phoneNumbers"];
_emailAddresses = [coder decodeObjectForKey:@"emailAddresses"];
NSData *data = [coder decodeObjectForKey:@"contactThumbnail"];
if (!data)
{
// Check the legacy storage.
data = [coder decodeObjectForKey:@"contactBookThumbnail"];
}
if (data)
{
contactThumbnail = [UIImage imageWithData:data];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:_contactID forKey:@"contactID"];
[coder encodeObject:_displayName forKey:@"displayName"];
if (matrixIdField)
{
[coder encodeObject:matrixIdField forKey:@"matrixIdField"];
}
if (_phoneNumbers.count)
{
[coder encodeObject:_phoneNumbers forKey:@"phoneNumbers"];
}
if (_emailAddresses.count)
{
[coder encodeObject:_emailAddresses forKey:@"emailAddresses"];
}
if (contactThumbnail)
{
@autoreleasepool
{
NSData *data = UIImageJPEGRepresentation(contactThumbnail, 0.8);
[coder encodeObject:data forKey:@"contactThumbnail"];
}
}
}
@end
@@ -0,0 +1,50 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <UIKit/UIKit.h>
@interface MXKContactField : NSObject<NSCoding>
/**
The identifier of the contact to whom the data belongs to.
*/
@property (nonatomic, readonly) NSString *contactID;
/**
The linked matrix identifier if any
*/
@property (nonatomic, readwrite) NSString *matrixID;
/**
The matrix avatar url (Matrix Content URI), nil by default.
*/
@property (nonatomic) NSString* matrixAvatarURL;
/**
The current avatar downloaded by using the avatar url if any
*/
@property (nonatomic, readonly) UIImage *avatarImage;
- (id)initWithContactID:(NSString*)contactID matrixID:(NSString*)matrixID;
- (void)loadAvatarWithSize:(CGSize)avatarSize;
/**
Reset the current avatar. May be used in case of the matrix avatar url change.
A new avatar will be automatically restored from the matrix data.
*/
- (void)resetMatrixAvatar;
@end
@@ -0,0 +1,235 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKContactField.h"
@import MatrixSDK.MXMediaManager;
#import "MXKContactManager.h"
@interface MXKContactField()
{
// Tell whether we already check the contact avatar definition.
BOOL shouldCheckAvatarURL;
// The media manager of the session used to retrieve the contect avatar url
// This manager is used to download this avatar if need
MXMediaManager *mediaManager;
// The current download id
NSString *downloadId;
}
@end
@implementation MXKContactField
- (void)initFields
{
// init members
_contactID = nil;
_matrixID = nil;
[self resetMatrixAvatar];
}
- (id)initWithContactID:(NSString*)contactID matrixID:(NSString*)matrixID
{
self = [super init];
if (self)
{
[self initFields];
_contactID = contactID;
_matrixID = matrixID;
}
return self;
}
- (void)resetMatrixAvatar
{
_avatarImage = nil;
_matrixAvatarURL = nil;
shouldCheckAvatarURL = YES;
mediaManager = nil;
downloadId = nil;
}
- (void)loadAvatarWithSize:(CGSize)avatarSize
{
// Check whether the avatar image is already set
if (_avatarImage)
{
return;
}
// Sanity check
if (_matrixID)
{
if (shouldCheckAvatarURL)
{
// Consider here all sessions reported into contact manager
NSArray* mxSessions = [MXKContactManager sharedManager].mxSessions;
if (mxSessions.count)
{
// Check whether a matrix user is already known
MXUser* user;
MXSession *mxSession;
for (mxSession in mxSessions)
{
user = [mxSession userWithUserId:_matrixID];
if (user)
{
_matrixAvatarURL = user.avatarUrl;
if (_matrixAvatarURL)
{
shouldCheckAvatarURL = NO;
mediaManager = mxSession.mediaManager;
[self downloadAvatarImage:avatarSize];
}
break;
}
}
// Trigger a server request if this url has not been found.
if (shouldCheckAvatarURL)
{
MXWeakify(self);
[mxSession.matrixRestClient avatarUrlForUser:_matrixID
success:^(NSString *mxAvatarUrl) {
MXStrongifyAndReturnIfNil(self);
self.matrixAvatarURL = mxAvatarUrl;
self->shouldCheckAvatarURL = NO;
self->mediaManager = mxSession.mediaManager;
[self downloadAvatarImage:avatarSize];
} failure:nil];
}
}
}
else if (_matrixAvatarURL)
{
[self downloadAvatarImage:avatarSize];
}
// Do nothing if the avatar url has been checked, and it is null.
}
}
- (void)downloadAvatarImage:(CGSize)avatarSize
{
// the avatar image is already done
if (_avatarImage)
{
return;
}
if (_matrixAvatarURL)
{
NSString *cacheFilePath = [MXMediaManager thumbnailCachePathForMatrixContentURI:_matrixAvatarURL
andType:nil
inFolder:kMXMediaManagerAvatarThumbnailFolder
toFitViewSize:avatarSize
withMethod:MXThumbnailingMethodCrop];
_avatarImage = [MXMediaManager loadPictureFromFilePath:cacheFilePath];
// the image is already in the cache
if (_avatarImage)
{
MXWeakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
MXStrongifyAndReturnIfNil(self);
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKContactThumbnailUpdateNotification object:self.contactID userInfo:nil];
});
}
else
{
NSString *downloadId = [MXMediaManager thumbnailDownloadIdForMatrixContentURI:_matrixAvatarURL inFolder:kMXMediaManagerAvatarThumbnailFolder toFitViewSize:avatarSize withMethod:MXThumbnailingMethodCrop];
MXMediaLoader* loader = [MXMediaManager existingDownloaderWithIdentifier:downloadId];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXMediaLoaderStateDidChangeNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onMediaDownloadEnd:) name:kMXMediaLoaderStateDidChangeNotification object:loader];
if (!loader && mediaManager)
{
[mediaManager downloadThumbnailFromMatrixContentURI:_matrixAvatarURL
withType:nil
inFolder:kMXMediaManagerAvatarThumbnailFolder
toFitViewSize:avatarSize
withMethod:MXThumbnailingMethodCrop
success:nil
failure:nil];
}
}
}
}
- (void)onMediaDownloadEnd:(NSNotification *)notif
{
MXMediaLoader *loader = (MXMediaLoader*)notif.object;
if ([loader.downloadId isEqualToString:downloadId])
{
// update the image
switch (loader.state) {
case MXMediaLoaderStateDownloadCompleted:
{
UIImage *image = [MXMediaManager loadPictureFromFilePath:loader.downloadOutputFilePath];
if (image)
{
_avatarImage = image;
MXWeakify(self);
dispatch_async(dispatch_get_main_queue(), ^{
MXStrongifyAndReturnIfNil(self);
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKContactThumbnailUpdateNotification object:self.contactID userInfo:nil];
});
}
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXMediaLoaderStateDidChangeNotification object:nil];
downloadId = nil;
break;
}
case MXMediaLoaderStateDownloadFailed:
case MXMediaLoaderStateCancelled:
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXMediaLoaderStateDidChangeNotification object:nil];
downloadId = nil;
break;
default:
break;
}
}
}
#pragma mark NSCoding
- (id)initWithCoder:(NSCoder *)coder
{
if (self)
{
[self initFields];
_contactID = [coder decodeObjectForKey:@"contactID"];
_matrixID = [coder decodeObjectForKey:@"matrixID"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[coder encodeObject:_contactID forKey:@"contactID"];
[coder encodeObject:_matrixID forKey:@"matrixID"];
}
@end
@@ -0,0 +1,243 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <Contacts/Contacts.h>
#import <MatrixSDK/MatrixSDK.h>
#import "MXKSectionedContacts.h"
#import "MXKContact.h"
/**
Posted when the matrix contact list is loaded or updated.
The notification object is:
- a contact Id when a matrix contact has been added/updated/removed.
or
- nil when all matrix contacts are concerned.
*/
extern NSString * _Nonnull const kMXKContactManagerDidUpdateMatrixContactsNotification;
/**
Posted when the local contact list is loaded and updated.
The notification object is:
- a contact Id when a local contact has been added/updated/removed.
or
- nil when all local contacts are concerned.
*/
extern NSString * _Nonnull const kMXKContactManagerDidUpdateLocalContactsNotification;
/**
Posted when local contact matrix ids is updated.
The notification object is:
- a contact Id when a local contact has been added/updated/removed.
or
- nil when all local contacts are concerned.
*/
extern NSString * _Nonnull const kMXKContactManagerDidUpdateLocalContactMatrixIDsNotification;
/**
Posted when the presence of a matrix user linked at least to one contact has changed.
The notification object is the matrix Id. The `userInfo` dictionary contains an `MXPresenceString` object under the `kMXKContactManagerMatrixPresenceKey` key, representing the matrix user presence.
*/
extern NSString * _Nonnull const kMXKContactManagerMatrixUserPresenceChangeNotification;
extern NSString * _Nonnull const kMXKContactManagerMatrixPresenceKey;
/**
Posted when all phonenumbers of local contacts have been internationalized.
The notification object is nil.
*/
extern NSString * _Nonnull const kMXKContactManagerDidInternationalizeNotification;
/**
Used to identify the type of data when requesting MXKeyProvider
*/
extern NSString * _Nonnull const MXKContactManagerDataType;
/**
Define the contact creation for the room members
*/
typedef NS_ENUM(NSInteger, MXKContactManagerMXRoomSource) {
MXKContactManagerMXRoomSourceNone = 0, // the MXMember does not create any new contact.
MXKContactManagerMXRoomSourceDirectChats = 1, // the direct chat users have their own contact even if they are not defined in the device contacts book
MXKContactManagerMXRoomSourceAll = 2, // all the room members have their own contact even if they are not defined in the device contacts book
};
/**
This manager handles 2 kinds of contact list:
- The local contacts retrieved from the device phonebook.
- The matrix contacts retrieved from the matrix one-to-one rooms.
Note: The local contacts handling depends on the 'syncLocalContacts' and 'phonebookCountryCode' properties
of the shared application settings object '[MXKAppSettings standardAppSettings]'.
*/
@interface MXKContactManager : NSObject
/**
The shared instance of contact manager.
*/
+ (MXKContactManager* _Nonnull)sharedManager;
/**
Block called (if any) to discover the Matrix users bound to a set of third-party identifiers (email addresses, phone numbers).
If this property is unset, the contact manager will consider the potential identity server URL (see the `identityServer` property)
to build its own Restclient and trigger `lookup3PIDs` requests.
@param threepids the list of 3rd party ids: [[<(MX3PIDMedium)media1>, <(NSString*)address1>], [<(MX3PIDMedium)media2>, <(NSString*)address2>], ...].
@param success a block object called when the operation succeeds. It provides the array of the discovered users:
[[<(MX3PIDMedium)media>, <(NSString*)address>, <(NSString*)userId>], ...].
@param failure a block object called when the operation fails.
*/
typedef void(^MXKContactManagerDiscoverUsersBoundTo3PIDs)(NSArray<NSArray<NSString *> *> * _Nonnull threepids,
void (^ _Nonnull success)(NSArray<NSArray<NSString *> *> *_Nonnull),
void (^ _Nonnull failure)(NSError *_Nonnull));
@property (nonatomic, nullable) MXKContactManagerDiscoverUsersBoundTo3PIDs discoverUsersBoundTo3PIDsBlock;
/**
Define if the room member must have their dedicated contact even if they are not define in the device contacts book.
The default value is MXKContactManagerMXRoomSourceDirectChats;
*/
@property (nonatomic) MXKContactManagerMXRoomSource contactManagerMXRoomSource;
/**
Associated matrix sessions (empty by default).
*/
@property (nonatomic, readonly, nonnull) NSArray *mxSessions;
/**
The current list of the contacts extracted from matrix data. Depends on 'contactManagerMXRoomSource'.
*/
@property (nonatomic, readonly, nullable) NSArray *matrixContacts;
/**
The current list of the local contacts (nil by default until the contacts are loaded).
*/
@property (nonatomic, readonly, nullable) NSArray *localContacts;
/**
The current list of the local contacts who have contact methods which can be used to invite them or to discover matrix users.
*/
@property (nonatomic, readonly, nullable) NSArray *localContactsWithMethods;
/**
The contacts list obtained by splitting each local contact by contact method.
This list is alphabetically sorted.
Each contact has one and only one contact method.
*/
//- (void)localContactsSplitByContactMethod:(void (^)(NSArray<MXKContact*> *localContactsSplitByContactMethod))onComplete;
@property (nonatomic, readonly, nullable) NSArray *localContactsSplitByContactMethod;
/**
The current list of the contacts for whom a direct chat exists.
*/
@property (nonatomic, readonly, nonnull) NSArray *directMatrixContacts;
/// Flag to allow local contacts access or not. Default value is YES.
@property (nonatomic, assign) BOOL allowLocalContactsAccess;
/**
Add/remove matrix session. The matrix contact list is automatically updated (see kMXKContactManagerDidUpdateMatrixContactsNotification event).
*/
- (void)addMatrixSession:(MXSession* _Nonnull)mxSession;
- (void)removeMatrixSession:(MXSession* _Nonnull)mxSession;
/**
Takes into account the state of the identity service's terms, local contacts access authorization along with
whether the user has left the app for the Settings app to update the contacts access, and enables/disables
the `syncLocalContacts` property of `MXKAppSettings` when necessary.
@param mxSession The session who's identity service shall be used.
*/
- (void)validateSyncLocalContactsStateForSession:(MXSession *)mxSession;
/**
Load and/or refresh the local contacts. Observe kMXKContactManagerDidUpdateLocalContactsNotification to know when local contacts are available.
*/
- (void)refreshLocalContacts;
/**
Delete contacts info
*/
- (void)reset;
/**
Get contact by its identifier.
@param contactID the contact identifier.
@return the contact defined with the provided id.
*/
- (MXKContact* _Nullable)contactWithContactID:(NSString* _Nonnull)contactID;
/**
Refresh matrix IDs for a specific local contact. See kMXKContactManagerDidUpdateLocalContactMatrixIDsNotification
posted when update is done.
@param contact the local contact to refresh.
*/
- (void)updateMatrixIDsForLocalContact:(MXKContact* _Nonnull)contact;
/**
Refresh matrix IDs for all local contacts. See kMXKContactManagerDidUpdateLocalContactMatrixIDsNotification
posted when update for all local contacts is done.
*/
- (void)updateMatrixIDsForAllLocalContacts;
/**
The contacts list obtained by splitting each local contact by contact method.
This list is alphabetically sorted.
Each contact has one and only one contact method.
*/
//- (void)localContactsSplitByContactMethod:(void (^)(NSArray<MXKContact*> *localContactsSplitByContactMethod))onComplete;
/**
Sort a contacts array in sectioned arrays to be displayable in a UITableview
*/
- (MXKSectionedContacts* _Nullable)getSectionedContacts:(NSArray* _Nonnull)contactList;
/**
Sort alphabetically an array of contacts.
@param contactsArray the array of contacts to sort.
*/
- (void)sortAlphabeticallyContacts:(NSMutableArray<MXKContact*> * _Nonnull)contactsArray;
/**
Sort an array of contacts by last active, with "active now" first.
...and then alphabetically.
@param contactsArray the array of contacts to sort.
*/
- (void)sortContactsByLastActiveInformation:(NSMutableArray<MXKContact*> * _Nonnull)contactsArray;
/**
Refresh the international phonenumber of the local contacts (See kMXKContactManagerDidInternationalizeNotification).
@param countryCode the country code.
*/
- (void)internationalizePhoneNumbers:(NSString* _Nonnull)countryCode;
/**
Request user permission for syncing local contacts.
@param viewController the view controller to attach the dialog to the user.
@param handler the block called with the result of requesting access
*/
+ (void)requestUserConfirmationForLocalContactsSyncInViewController:(UIViewController* _Nonnull)viewController
completionHandler:(void (^_Nonnull)(BOOL granted))handler;
@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <UIKit/UIKit.h>
#import "MXKContactField.h"
@interface MXKEmail : MXKContactField
// email info (the address is stored in lowercase)
@property (nonatomic, readonly) NSString *type;
@property (nonatomic, readonly) NSString *emailAddress;
- (id)initWithEmailAddress:(NSString*)anEmailAddress type:(NSString*)aType contactID:(NSString*)aContactID matrixID:(NSString*)matrixID;
- (BOOL)matchedWithPatterns:(NSArray*)patterns;
@end
@@ -0,0 +1,91 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKEmail.h"
@implementation MXKEmail
- (id)init
{
self = [super init];
if (self)
{
_emailAddress = nil;
_type = nil;
}
return self;
}
- (id)initWithEmailAddress:(NSString*)anEmailAddress type:(NSString*)aType contactID:(NSString*)aContactID matrixID:(NSString*)matrixID
{
self = [super initWithContactID:aContactID matrixID:matrixID];
if (self)
{
_emailAddress = [anEmailAddress lowercaseString];
_type = aType;
}
return self;
}
- (BOOL)matchedWithPatterns:(NSArray*)patterns
{
// no number -> cannot match
if (_emailAddress.length == 0)
{
return NO;
}
if (patterns.count > 0)
{
for(NSString *pattern in patterns)
{
if ([_emailAddress rangeOfString:pattern options:NSCaseInsensitiveSearch].location == NSNotFound)
{
return NO;
}
}
}
return YES;
}
#pragma mark NSCoding
- (id)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self)
{
_type = [coder decodeObjectForKey:@"type"];
_emailAddress = [[coder decodeObjectForKey:@"emailAddress"] lowercaseString];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[super encodeWithCoder:coder];
[coder encodeObject:_type forKey:@"type"];
[coder encodeObject:_emailAddress forKey:@"emailAddress"];
}
@end
@@ -0,0 +1,78 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "MXKContactField.h"
@class NBPhoneNumber;
@interface MXKPhoneNumber : MXKContactField
/**
The phone number information
*/
@property (nonatomic, readonly) NSString *type;
@property (nonatomic, readonly) NSString *textNumber;
@property (nonatomic, readonly) NSString *cleanedPhonenumber;
/**
When the number is considered to be a possible number. We expose here
the corresponding NBPhoneNumber instance. Use the NBPhoneNumberUtil interface
to format this phone number, or check whether the number is actually a
valid number.
*/
@property (nonatomic, readonly) NBPhoneNumber* nbPhoneNumber;
/**
The default ISO 3166-1 country code used to parse the text number,
and create the nbPhoneNumber instance.
*/
@property (nonatomic) NSString *defaultCountryCode;
/**
The Mobile Station International Subscriber Directory Number.
Available when the nbPhoneNumber is not nil.
*/
@property (nonatomic, readonly) NSString *msisdn;
/**
Create a new MXKPhoneNumber instance
@param textNumber the phone number
@param type the phone number type
@param contactID The identifier of the contact to whom the data belongs to.
@param matrixID The linked matrix identifier if any.
*/
- (id)initWithTextNumber:(NSString*)textNumber type:(NSString*)type contactID:(NSString*)contactID matrixID:(NSString*)matrixID;
/**
Return YES when all the provided patterns are found in the phone number or its msisdn.
@param patterns an array of patterns (The potential "+" (or "00") prefix is ignored during the msisdn handling).
*/
- (BOOL)matchedWithPatterns:(NSArray*)patterns;
/**
Tell whether the phone number or its msisdn has the provided prefix.
@param prefix a non empty string (The potential "+" (or "00") prefix is ignored during the msisdn handling).
@return YES when the phone number or its msisdn has the provided prefix.
*/
- (BOOL)hasPrefix:(NSString*)prefix;
@end
@@ -0,0 +1,213 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKPhoneNumber.h"
@import libPhoneNumber_iOS;
@implementation MXKPhoneNumber
@synthesize msisdn;
- (id)initWithTextNumber:(NSString*)textNumber type:(NSString*)type contactID:(NSString*)contactID matrixID:(NSString*)matrixID
{
self = [super initWithContactID:contactID matrixID:matrixID];
if (self)
{
_type = type ? type : @"";
_textNumber = textNumber ? textNumber : @"" ;
_cleanedPhonenumber = [MXKPhoneNumber cleanPhonenumber:_textNumber];
_defaultCountryCode = nil;
msisdn = nil;
_nbPhoneNumber = [[NBPhoneNumberUtil sharedInstance] parse:_cleanedPhonenumber defaultRegion:nil error:nil];
}
return self;
}
// remove the unuseful characters in a phonenumber
+ (NSString*)cleanPhonenumber:(NSString*)phoneNumber
{
// sanity check
if (nil == phoneNumber)
{
return nil;
}
// empty string
if (0 == [phoneNumber length])
{
return @"";
}
static NSCharacterSet *invertedPhoneCharSet = nil;
if (!invertedPhoneCharSet)
{
invertedPhoneCharSet = [[NSCharacterSet characterSetWithCharactersInString:@"0123456789+*#,()"] invertedSet];
}
return [[phoneNumber componentsSeparatedByCharactersInSet:invertedPhoneCharSet] componentsJoinedByString:@""];
}
- (BOOL)matchedWithPatterns:(NSArray*)patterns
{
// no number -> cannot match
if (_textNumber.length == 0)
{
return NO;
}
if (patterns.count > 0)
{
for (NSString *pattern in patterns)
{
if ([_textNumber rangeOfString:pattern].location == NSNotFound)
{
NSString *cleanPattern = [[pattern componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] componentsJoinedByString:@""];
if ([_cleanedPhonenumber rangeOfString:cleanPattern].location == NSNotFound)
{
NSString *msisdnPattern;
if ([cleanPattern hasPrefix:@"+"])
{
msisdnPattern = [cleanPattern substringFromIndex:1];
}
else if ([cleanPattern hasPrefix:@"00"])
{
msisdnPattern = [cleanPattern substringFromIndex:2];
}
else
{
msisdnPattern = cleanPattern;
}
// Check the msisdn
if (!self.msisdn || !msisdnPattern.length || [self.msisdn rangeOfString:msisdnPattern].location == NSNotFound)
{
return NO;
}
}
}
}
}
return YES;
}
- (BOOL)hasPrefix:(NSString*)prefix
{
// no number -> cannot match
if (_textNumber.length == 0)
{
return NO;
}
if ([_textNumber hasPrefix:prefix])
{
return YES;
}
// Remove whitespace before checking the cleaned phone number
prefix = [[prefix componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] componentsJoinedByString:@""];
if ([_cleanedPhonenumber hasPrefix:prefix])
{
return YES;
}
if (self.msisdn)
{
if ([prefix hasPrefix:@"+"])
{
prefix = [prefix substringFromIndex:1];
}
else if ([prefix hasPrefix:@"00"])
{
prefix = [prefix substringFromIndex:2];
}
return [self.msisdn hasPrefix:prefix];
}
return NO;
}
- (void)setDefaultCountryCode:(NSString *)defaultCountryCode
{
if (![defaultCountryCode isEqualToString:_defaultCountryCode])
{
_nbPhoneNumber = [[NBPhoneNumberUtil sharedInstance] parse:_cleanedPhonenumber defaultRegion:defaultCountryCode error:nil];
_defaultCountryCode = defaultCountryCode;
msisdn = nil;
}
}
- (NSString*)msisdn
{
if (!msisdn && _nbPhoneNumber)
{
NSString *e164 = [[NBPhoneNumberUtil sharedInstance] format:_nbPhoneNumber numberFormat:NBEPhoneNumberFormatE164 error:nil];
if ([e164 hasPrefix:@"+"])
{
msisdn = [e164 substringFromIndex:1];
}
else if ([e164 hasPrefix:@"00"])
{
msisdn = [e164 substringFromIndex:2];
}
}
return msisdn;
}
#pragma mark NSCoding
- (id)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self)
{
_type = [coder decodeObjectForKey:@"type"];
_textNumber = [coder decodeObjectForKey:@"textNumber"];
_cleanedPhonenumber = [coder decodeObjectForKey:@"cleanedPhonenumber"];
_defaultCountryCode = [coder decodeObjectForKey:@"countryCode"];
_nbPhoneNumber = [[NBPhoneNumberUtil sharedInstance] parse:_cleanedPhonenumber defaultRegion:_defaultCountryCode error:nil];
msisdn = nil;
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)coder
{
[super encodeWithCoder:coder];
[coder encodeObject:_type forKey:@"type"];
[coder encodeObject:_textNumber forKey:@"textNumber"];
[coder encodeObject:_cleanedPhonenumber forKey:@"cleanedPhonenumber"];
[coder encodeObject:_defaultCountryCode forKey:@"countryCode"];
}
@end
@@ -0,0 +1,33 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "MXKContact.h"
@interface MXKSectionedContacts : NSObject {
int contactsCount;
NSArray<NSString*> *sectionTitles;
NSArray<NSArray<MXKContact*>*> *sectionedContacts;
}
@property (nonatomic, readonly) int contactsCount;
@property (nonatomic, readonly) NSArray<NSString*> *sectionTitles;
@property (nonatomic, readonly) NSArray<NSArray<MXKContact*>*> *sectionedContacts;
- (instancetype)initWithContacts:(NSArray<NSArray<MXKContact*>*> *)inSectionedContacts andTitles:(NSArray<NSString*> *)titles andCount:(int)count;
@end
@@ -0,0 +1,32 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKSectionedContacts.h"
@implementation MXKSectionedContacts
@synthesize contactsCount, sectionTitles, sectionedContacts;
-(id)initWithContacts:(NSArray<NSArray<MXKContact*> *> *)inSectionedContacts andTitles:(NSArray<NSString *> *)titles andCount:(int)count {
if (self = [super init]) {
contactsCount = count;
sectionedContacts = inSectionedContacts;
sectionTitles = titles;
}
return self;
}
@end
@@ -0,0 +1,24 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKGroupCellDataStoring.h"
/**
`MXKGroupCellData` modelised the data for a `MXKGroupTableViewCell` cell.
*/
@interface MXKGroupCellData : MXKCellData <MXKGroupCellDataStoring>
@end
@@ -0,0 +1,49 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKGroupCellData.h"
#import "MXKSessionGroupsDataSource.h"
@implementation MXKGroupCellData
@synthesize group, groupsDataSource, groupDisplayname, sortingDisplayname;
- (instancetype)initWithGroup:(MXGroup*)theGroup andGroupsDataSource:(MXKSessionGroupsDataSource*)theGroupsDataSource
{
self = [self init];
if (self)
{
groupsDataSource = theGroupsDataSource;
[self updateWithGroup:theGroup];
}
return self;
}
- (void)updateWithGroup:(MXGroup*)theGroup
{
group = theGroup;
groupDisplayname = sortingDisplayname = group.profile.name;
if (!groupDisplayname.length)
{
groupDisplayname = group.groupId;
// Ignore the prefix '+' of the group id during sorting.
sortingDisplayname = [groupDisplayname substringFromIndex:1];
}
}
@end
@@ -0,0 +1,53 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <MatrixSDK/MatrixSDK.h>
#import "MXKCellData.h"
@class MXKSessionGroupsDataSource;
/**
`MXKGroupCellDataStoring` defines a protocol a class must conform in order to store group cell data
managed by `MXKSessionGroupsDataSource`.
*/
@protocol MXKGroupCellDataStoring <NSObject>
@property (nonatomic, weak, readonly) MXKSessionGroupsDataSource *groupsDataSource;
@property (nonatomic, readonly) MXGroup *group;
@property (nonatomic, readonly) NSString *groupDisplayname;
@property (nonatomic, readonly) NSString *sortingDisplayname;
#pragma mark - Public methods
/**
Create a new `MXKCellData` object for a new group cell.
@param group the `MXGroup` object that has data about the group.
@param groupsDataSource the `MXKSessionGroupsDataSource` object that will use this instance.
@return the newly created instance.
*/
- (instancetype)initWithGroup:(MXGroup*)group andGroupsDataSource:(MXKSessionGroupsDataSource*)groupsDataSource;
/**
The `MXKSessionGroupsDataSource` object calls this method when the group data has been updated.
@param group the updated group.
*/
- (void)updateWithGroup:(MXGroup*)group;
@end
@@ -0,0 +1,94 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKDataSource.h"
#import "MXKGroupCellData.h"
/**
Identifier to use for cells that display a group.
*/
extern NSString *const kMXKGroupCellIdentifier;
/**
'MXKSessionGroupsDataSource' is a base class to handle the groups of a matrix session.
A 'MXKSessionGroupsDataSource' instance provides the data source for `MXKGroupListViewController`.
A section is created to handle the invitations to a group, the first one if any.
*/
@interface MXKSessionGroupsDataSource : MXKDataSource <UITableViewDataSource>
{
@protected
/**
The current list of the group invitations (sorted in the alphabetic order).
This list takes into account potential filter defined by`patternsList`.
*/
NSMutableArray<MXKGroupCellData*> *groupsInviteCellDataArray;
/**
The current displayed list of the joined groups (sorted in the alphabetic order).
This list takes into account potential filter defined by`patternsList`.
*/
NSMutableArray<MXKGroupCellData*> *groupsCellDataArray;
}
@property (nonatomic) NSInteger groupInvitesSection;
@property (nonatomic) NSInteger joinedGroupsSection;
#pragma mark - Life cycle
/**
Refresh all the groups summary.
The group data are not synced with the server, use this method to refresh them according to your needs.
@param completion the block to execute when a request has been done for each group (whatever the result of the requests).
You may specify nil for this parameter.
*/
- (void)refreshGroupsSummary:(void (^)(void))completion;
/**
Filter the current groups list according to the provided patterns.
When patterns are not empty, the search result is stored in `filteredGroupsCellDataArray`,
this array provides then data for the cells served by `MXKSessionGroupsDataSource`.
@param patternsList the list of patterns (`NSString` instances) to match with. Set nil to cancel search.
*/
- (void)searchWithPatterns:(NSArray*)patternsList;
/**
Get the data for the cell at the given index path.
@param indexPath the index of the cell in the table
@return the cell data
*/
- (id<MXKGroupCellDataStoring>)cellDataAtIndex:(NSIndexPath*)indexPath;
/**
Get the index path of the cell related to the provided groupId.
@param groupId the group identifier.
@return indexPath the index of the cell (nil if not found).
*/
- (NSIndexPath*)cellIndexPathWithGroupId:(NSString*)groupId;
/**
Leave the group displayed at the provided path.
@param indexPath the index of the group cell in the table
*/
- (void)leaveGroupAtIndexPath:(NSIndexPath *)indexPath;
@end
@@ -0,0 +1,611 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKSessionGroupsDataSource.h"
#import "NSBundle+MatrixKit.h"
#import "MXKConstants.h"
#import "MXKSwiftHeader.h"
#pragma mark - Constant definitions
NSString *const kMXKGroupCellIdentifier = @"kMXKGroupCellIdentifier";
@interface MXKSessionGroupsDataSource ()
{
/**
Internal array used to regulate change notifications.
Cell data changes are stored instantly in this array.
We wait at least for 500 ms between two notifications of the delegate.
*/
NSMutableArray *internalCellDataArray;
/*
Timer to not notify the delegate on every changes.
*/
NSTimer *timer;
/*
Tells whether some changes must be notified.
*/
BOOL isDataChangePending;
/**
Store the current search patterns list.
*/
NSArray* searchPatternsList;
}
@end
@implementation MXKSessionGroupsDataSource
- (instancetype)initWithMatrixSession:(MXSession *)matrixSession
{
self = [super initWithMatrixSession:matrixSession];
if (self)
{
internalCellDataArray = [NSMutableArray array];
groupsCellDataArray = [NSMutableArray array];
groupsInviteCellDataArray = [NSMutableArray array];
isDataChangePending = NO;
// Set default data and view classes
[self registerCellDataClass:MXKGroupCellData.class forCellIdentifier:kMXKGroupCellIdentifier];
}
return self;
}
- (void)destroy
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
groupsCellDataArray = nil;
groupsInviteCellDataArray = nil;
internalCellDataArray = nil;
searchPatternsList = nil;
[timer invalidate];
timer = nil;
[super destroy];
}
- (void)didMXSessionStateChange
{
if (MXSessionStateRunning <= self.mxSession.state)
{
// Check whether some data have been already load
if (0 == internalCellDataArray.count)
{
[self loadData];
}
else if (self.mxSession.state == MXSessionStateRunning)
{
// Refresh the group data
[self refreshGroupsSummary:nil];
}
}
}
#pragma mark -
- (void)refreshGroupsSummary:(void (^)(void))completion
{
MXLogDebug(@"[MXKSessionGroupsDataSource] refreshGroupsSummary");
__block NSUInteger count = internalCellDataArray.count;
if (count)
{
for (id<MXKGroupCellDataStoring> groupData in internalCellDataArray)
{
// Force the matrix session to refresh the group summary.
[self.mxSession updateGroupSummary:groupData.group success:^{
if (completion && !(--count))
{
// All the requests have been done.
completion ();
}
} failure:^(NSError *error) {
MXLogDebug(@"[MXKSessionGroupsDataSource] refreshGroupsSummary: group summary update failed %@", groupData.group.groupId);
if (completion && !(--count))
{
// All the requests have been done.
completion ();
}
}];
}
}
else if (completion)
{
completion();
}
}
- (void)searchWithPatterns:(NSArray*)patternsList
{
if (patternsList.count)
{
searchPatternsList = patternsList;
}
else
{
searchPatternsList = nil;
}
[self onCellDataChange];
}
- (id<MXKGroupCellDataStoring>)cellDataAtIndex:(NSIndexPath*)indexPath
{
id<MXKGroupCellDataStoring> groupData;
if (indexPath.section == _groupInvitesSection)
{
if (indexPath.row < groupsInviteCellDataArray.count)
{
groupData = groupsInviteCellDataArray[indexPath.row];
}
}
else if (indexPath.section == _joinedGroupsSection)
{
if (indexPath.row < groupsCellDataArray.count)
{
groupData = groupsCellDataArray[indexPath.row];
}
}
return groupData;
}
- (NSIndexPath*)cellIndexPathWithGroupId:(NSString*)groupId
{
// Look for the cell
if (_groupInvitesSection != -1)
{
for (NSInteger index = 0; index < groupsInviteCellDataArray.count; index ++)
{
id<MXKGroupCellDataStoring> groupData = groupsInviteCellDataArray[index];
if ([groupId isEqualToString:groupData.group.groupId])
{
// Got it
return [NSIndexPath indexPathForRow:index inSection:_groupInvitesSection];
}
}
}
if (_joinedGroupsSection != -1)
{
for (NSInteger index = 0; index < groupsCellDataArray.count; index ++)
{
id<MXKGroupCellDataStoring> groupData = groupsCellDataArray[index];
if ([groupId isEqualToString:groupData.group.groupId])
{
// Got it
return [NSIndexPath indexPathForRow:index inSection:_joinedGroupsSection];
}
}
}
return nil;
}
#pragma mark - Groups processing
- (void)loadData
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionNewGroupInviteNotification object:self.mxSession];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidJoinGroupNotification object:self.mxSession];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidLeaveGroupNotification object:self.mxSession];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidUpdateGroupSummaryNotification object:self.mxSession];
// Reset the table
[internalCellDataArray removeAllObjects];
// Retrieve the MXKCellData class to manage the data
Class class = [self cellDataClassForCellIdentifier:kMXKGroupCellIdentifier];
NSAssert([class conformsToProtocol:@protocol(MXKGroupCellDataStoring)], @"MXKSessionGroupsDataSource only manages MXKCellData that conforms to MXKGroupCellDataStoring protocol");
// Listen to MXSession groups changes
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onNewGroupInvite:) name:kMXSessionNewGroupInviteNotification object:self.mxSession];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didJoinGroup:) name:kMXSessionDidJoinGroupNotification object:self.mxSession];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didLeaveGroup:) name:kMXSessionDidLeaveGroupNotification object:self.mxSession];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didUpdateGroup:) name:kMXSessionDidUpdateGroupSummaryNotification object:self.mxSession];
NSDate *startDate = [NSDate date];
NSArray *groups = self.mxSession.groups;
for (MXGroup *group in groups)
{
id<MXKGroupCellDataStoring> cellData = [[class alloc] initWithGroup:group andGroupsDataSource:self];
if (cellData)
{
[internalCellDataArray addObject:cellData];
// Force the matrix session to refresh the group summary.
[self.mxSession updateGroupSummary:group success:nil failure:^(NSError *error) {
MXLogDebug(@"[MXKSessionGroupsDataSource] loadData: group summary update failed %@", group.groupId);
}];
}
}
MXLogDebug(@"[MXKSessionGroupsDataSource] Loaded %tu groups in %.3fms", groups.count, [[NSDate date] timeIntervalSinceDate:startDate] * 1000);
[self sortCellData];
[self onCellDataChange];
}
- (void)didUpdateGroup:(NSNotification *)notif
{
MXGroup *group = notif.userInfo[kMXSessionNotificationGroupKey];
if (group)
{
id<MXKGroupCellDataStoring> groupData = [self cellDataWithGroupId:group.groupId];
if (groupData)
{
[groupData updateWithGroup:group];
}
else
{
MXLogDebug(@"[MXKSessionGroupsDataSource] didUpdateGroup: Cannot find the changed group for %@ (%@). It is probably not managed by this group data source", group.groupId, group);
return;
}
}
[self sortCellData];
[self onCellDataChange];
}
- (void)onNewGroupInvite:(NSNotification *)notif
{
MXGroup *group = notif.userInfo[kMXSessionNotificationGroupKey];
if (group)
{
// Add the group if there is not yet a cell for it
id<MXKGroupCellDataStoring> groupData = [self cellDataWithGroupId:group.groupId];
if (nil == groupData)
{
MXLogDebug(@"MXKSessionGroupsDataSource] Add new group invite: %@", group.groupId);
// Retrieve the MXKCellData class to manage the data
Class class = [self cellDataClassForCellIdentifier:kMXKGroupCellIdentifier];
id<MXKGroupCellDataStoring> cellData = [[class alloc] initWithGroup:group andGroupsDataSource:self];
if (cellData)
{
[internalCellDataArray addObject:cellData];
[self sortCellData];
[self onCellDataChange];
}
}
}
}
- (void)didJoinGroup:(NSNotification *)notif
{
MXGroup *group = notif.userInfo[kMXSessionNotificationGroupKey];
if (group)
{
id<MXKGroupCellDataStoring> groupData = [self cellDataWithGroupId:group.groupId];
if (groupData)
{
MXLogDebug(@"MXKSessionGroupsDataSource] Update joined room: %@", group.groupId);
[groupData updateWithGroup:group];
}
else
{
MXLogDebug(@"MXKSessionGroupsDataSource] Add new joined invite: %@", group.groupId);
// Retrieve the MXKCellData class to manage the data
Class class = [self cellDataClassForCellIdentifier:kMXKGroupCellIdentifier];
id<MXKGroupCellDataStoring> cellData = [[class alloc] initWithGroup:group andGroupsDataSource:self];
if (cellData)
{
[internalCellDataArray addObject:cellData];
}
}
[self sortCellData];
[self onCellDataChange];
}
}
- (void)didLeaveGroup:(NSNotification *)notif
{
NSString *groupId = notif.userInfo[kMXSessionNotificationGroupIdKey];
if (groupId)
{
[self removeGroup:groupId];
}
}
- (void)removeGroup:(NSString*)groupId
{
id<MXKGroupCellDataStoring> groupData = [self cellDataWithGroupId:groupId];
if (groupData)
{
MXLogDebug(@"MXKSessionGroupsDataSource] Remove left group: %@", groupId);
[internalCellDataArray removeObject:groupData];
[self sortCellData];
[self onCellDataChange];
}
}
- (void)onCellDataChange
{
isDataChangePending = NO;
// Check no notification was done recently.
// Note: do not wait in case of search
if (timer == nil || searchPatternsList)
{
[timer invalidate];
timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(checkPendingUpdate:) userInfo:nil repeats:NO];
// Prepare cell data array, and notify the delegate.
[self prepareCellDataAndNotifyChanges];
}
else
{
isDataChangePending = YES;
}
}
- (IBAction)checkPendingUpdate:(id)sender
{
[timer invalidate];
timer = nil;
if (isDataChangePending)
{
[self onCellDataChange];
}
}
- (void)sortCellData
{
// Order alphabetically the groups
[internalCellDataArray sortUsingComparator:^NSComparisonResult(id<MXKGroupCellDataStoring> cellData1, id<MXKGroupCellDataStoring> cellData2)
{
if (cellData1.sortingDisplayname.length && cellData2.sortingDisplayname.length)
{
return [cellData1.sortingDisplayname compare:cellData2.sortingDisplayname options:NSCaseInsensitiveSearch];
}
else if (cellData1.sortingDisplayname.length)
{
return NSOrderedAscending;
}
else if (cellData2.sortingDisplayname.length)
{
return NSOrderedDescending;
}
return NSOrderedSame;
}];
}
- (void)prepareCellDataAndNotifyChanges
{
// Prepare the cell data arrays by considering the potential filter.
[groupsInviteCellDataArray removeAllObjects];
[groupsCellDataArray removeAllObjects];
for (id<MXKGroupCellDataStoring> groupData in internalCellDataArray)
{
BOOL isKept = !searchPatternsList;
for (NSString* pattern in searchPatternsList)
{
if (groupData.groupDisplayname && [groupData.groupDisplayname rangeOfString:pattern options:NSCaseInsensitiveSearch].location != NSNotFound)
{
isKept = YES;
break;
}
}
if (isKept)
{
if (groupData.group.membership == MXMembershipInvite)
{
[groupsInviteCellDataArray addObject:groupData];
}
else
{
[groupsCellDataArray addObject:groupData];
}
}
}
// Update here data source state
if (state != MXKDataSourceStateReady)
{
state = MXKDataSourceStateReady;
if (self.delegate && [self.delegate respondsToSelector:@selector(dataSource:didStateChange:)])
{
[self.delegate dataSource:self didStateChange:state];
}
}
// And inform the delegate about the update
[self.delegate dataSource:self didCellChange:nil];
}
// Find the cell data that stores information about the given group id
- (id<MXKGroupCellDataStoring>)cellDataWithGroupId:(NSString*)groupId
{
id<MXKGroupCellDataStoring> theGroupData;
for (id<MXKGroupCellDataStoring> groupData in internalCellDataArray)
{
if ([groupData.group.groupId isEqualToString:groupId])
{
theGroupData = groupData;
break;
}
}
return theGroupData;
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
NSInteger count = 0;
_groupInvitesSection = _joinedGroupsSection = -1;
// Check whether all data sources are ready before rendering groups.
if (self.state == MXKDataSourceStateReady)
{
if (groupsInviteCellDataArray.count)
{
_groupInvitesSection = count++;
}
if (groupsCellDataArray.count)
{
_joinedGroupsSection = count++;
}
}
return count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (section == _groupInvitesSection)
{
return groupsInviteCellDataArray.count;
}
else if (section == _joinedGroupsSection)
{
return groupsCellDataArray.count;
}
return 0;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
NSString* sectionTitle = nil;
if (section == _groupInvitesSection)
{
sectionTitle = [MatrixKitL10n groupInviteSection];
}
else if (section == _joinedGroupsSection)
{
sectionTitle = [MatrixKitL10n groupSection];
}
return sectionTitle;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
id<MXKGroupCellDataStoring> groupData;
if (indexPath.section == _groupInvitesSection)
{
if (indexPath.row < groupsInviteCellDataArray.count)
{
groupData = groupsInviteCellDataArray[indexPath.row];
}
}
else if (indexPath.section == _joinedGroupsSection)
{
if (indexPath.row < groupsCellDataArray.count)
{
groupData = groupsCellDataArray[indexPath.row];
}
}
if (groupData)
{
NSString *cellIdentifier = [self.delegate cellReuseIdentifierForCellData:groupData];
if (cellIdentifier)
{
UITableViewCell<MXKCellRendering> *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
// Make sure we listen to user actions on the cell
cell.delegate = self;
// Make the bubble display the data
[cell render:groupData];
return cell;
}
}
// Return a fake cell to prevent app from crashing.
return [[UITableViewCell alloc] init];
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return NO if you do not want the specified item to be editable.
return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
{
[self leaveGroupAtIndexPath:indexPath];
}
}
- (void)leaveGroupAtIndexPath:(NSIndexPath *)indexPath
{
id<MXKGroupCellDataStoring> cellData = [self cellDataAtIndex:indexPath];
if (cellData.group)
{
__weak typeof(self) weakSelf = self;
[self.mxSession leaveGroup:cellData.group.groupId success:^{
if (weakSelf)
{
// Refresh the table content
typeof(self) self = weakSelf;
[self removeGroup:cellData.group.groupId];
}
} failure:^(NSError *error) {
MXLogDebug(@"[MXKSessionGroupsDataSource] Failed to leave group (%@)", cellData.group.groupId);
// Notify MatrixKit user
NSString *myUserId = self.mxSession.myUser.userId;
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];
}];
}
}
@end
+119
View File
@@ -0,0 +1,119 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <MatrixSDK/MatrixSDK.h>
typedef enum : NSUInteger {
MXK3PIDAuthStateUnknown,
MXK3PIDAuthStateTokenRequested,
MXK3PIDAuthStateTokenReceived,
MXK3PIDAuthStateTokenSubmitted,
MXK3PIDAuthStateAuthenticated
} MXK3PIDAuthState;
@interface MXK3PID : NSObject
/**
The type of the third party media.
*/
@property (nonatomic, readonly) MX3PIDMedium medium;
/**
The third party media (email address, msisdn,...).
*/
@property (nonatomic, readonly) NSString *address;
/**
The current client secret key used during third party validation.
*/
@property (nonatomic, readonly) NSString *clientSecret;
/**
The current session identifier during third party validation.
*/
@property (nonatomic, readonly) NSString *sid;
/**
The id of the user on Matrix.
nil if unknown or not yet resolved.
*/
@property (nonatomic) NSString *userId;
@property (nonatomic, readonly) MXK3PIDAuthState validationState;
/**
Initialise the instance with a 3PID.
@param medium the medium.
@param address the id of the contact on this medium.
@return the new instance.
*/
- (instancetype)initWithMedium:(NSString*)medium andAddress:(NSString*)address;
/**
Cancel the current request, and reset parameters
*/
- (void)cancelCurrentRequest;
/**
Start the validation process
The identity server will send a validation token by email or sms.
In case of email, the end user must click on the link in the received email
to validate their email address in order to be able to call add3PIDToUser successfully.
In case of phone number, the end user must send back the sms token
in order to be able to call add3PIDToUser successfully.
@param restClient used to make matrix API requests during validation process.
@param isDuringRegistration tell whether this request occurs during a registration flow.
@param nextLink the link the validation page will automatically open. Can be nil.
@param success A block object called when the operation succeeds.
@param failure A block object called when the operation fails.
*/
- (void)requestValidationTokenWithMatrixRestClient:(MXRestClient*)restClient
isDuringRegistration:(BOOL)isDuringRegistration
nextLink:(NSString*)nextLink
success:(void (^)(void))success
failure:(void (^)(NSError *error))failure;
/**
Submit the received validation token.
@param token the validation token.
@param success A block object called when the operation succeeds.
@param failure A block object called when the operation fails.
*/
- (void)submitValidationToken:(NSString *)token
success:(void (^)(void))success
failure:(void (^)(NSError *error))failure;
/**
Link a 3rd party id to the user.
@param bind whether the homeserver should also bind this third party identifier
to the account's Matrix ID with the identity server.
@param success A block object called when the operation succeeds. It provides the raw
server response.
@param failure A block object called when the operation fails.
*/
- (void)add3PIDToUser:(BOOL)bind
success:(void (^)(void))success
failure:(void (^)(NSError *error))failure;
@end
+316
View File
@@ -0,0 +1,316 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXK3PID.h"
@import libPhoneNumber_iOS;
@interface MXK3PID ()
{
MXRestClient *mxRestClient;
MXHTTPOperation *currentRequest;
}
@property (nonatomic) NSString *clientSecret;
@property (nonatomic) NSUInteger sendAttempt;
@property (nonatomic) NSString *sid;
@property (nonatomic) MXIdentityService *identityService;
@property (nonatomic) NSString *submitUrl;
@end
@implementation MXK3PID
- (instancetype)initWithMedium:(NSString *)medium andAddress:(NSString *)address
{
self = [super init];
if (self)
{
_medium = [medium copy];
_address = [address copy];
self.clientSecret = [MXTools generateSecret];
}
return self;
}
- (void)cancelCurrentRequest
{
_validationState = MXK3PIDAuthStateUnknown;
[currentRequest cancel];
currentRequest = nil;
mxRestClient = nil;
self.identityService = nil;
self.sendAttempt = 1;
self.sid = nil;
// Removed potential linked userId
self.userId = nil;
}
- (void)requestValidationTokenWithMatrixRestClient:(MXRestClient*)restClient
isDuringRegistration:(BOOL)isDuringRegistration
nextLink:(NSString*)nextLink
success:(void (^)(void))success
failure:(void (^)(NSError *error))failure
{
// Sanity Check
if (_validationState != MXK3PIDAuthStateTokenRequested && restClient)
{
// Reset if the current state is different than "Unknown"
if (_validationState != MXK3PIDAuthStateUnknown)
{
[self cancelCurrentRequest];
}
NSString *identityServer = restClient.identityServer;
if (identityServer)
{
// Use same identity server as REST client for validation token submission
self.identityService = [[MXIdentityService alloc] initWithIdentityServer:identityServer accessToken:nil andHomeserverRestClient:restClient];
}
if ([self.medium isEqualToString:kMX3PIDMediumEmail])
{
_validationState = MXK3PIDAuthStateTokenRequested;
mxRestClient = restClient;
currentRequest = [mxRestClient requestTokenForEmail:self.address isDuringRegistration:isDuringRegistration clientSecret:self.clientSecret sendAttempt:self.sendAttempt nextLink:nextLink success:^(NSString *sid) {
self->_validationState = MXK3PIDAuthStateTokenReceived;
self->currentRequest = nil;
self.sid = sid;
if (success)
{
success();
}
} failure:^(NSError *error) {
// Return in unknown state
self->_validationState = MXK3PIDAuthStateUnknown;
self->currentRequest = nil;
// Increment attempt counter
self.sendAttempt++;
if (failure)
{
failure (error);
}
}];
}
else if ([self.medium isEqualToString:kMX3PIDMediumMSISDN])
{
_validationState = MXK3PIDAuthStateTokenRequested;
mxRestClient = restClient;
NSString *phoneNumber = [NSString stringWithFormat:@"+%@", self.address];
currentRequest = [mxRestClient requestTokenForPhoneNumber:phoneNumber isDuringRegistration:isDuringRegistration countryCode:nil clientSecret:self.clientSecret sendAttempt:self.sendAttempt nextLink:nextLink success:^(NSString *sid, NSString *msisdn, NSString *submitUrl) {
self->_validationState = MXK3PIDAuthStateTokenReceived;
self->currentRequest = nil;
self.sid = sid;
self.submitUrl = submitUrl;
if (success)
{
success();
}
} failure:^(NSError *error) {
// Return in unknown state
self->_validationState = MXK3PIDAuthStateUnknown;
self->currentRequest = nil;
// Increment attempt counter
self.sendAttempt++;
if (failure)
{
failure (error);
}
}];
}
else
{
MXLogDebug(@"[MXK3PID] requestValidationToken: is not supported for this 3PID: %@ (%@)", self.address, self.medium);
}
}
else
{
MXLogDebug(@"[MXK3PID] Failed to request validation token for 3PID: %@ (%@), state: %lu", self.address, self.medium, (unsigned long)_validationState);
}
}
- (void)submitValidationToken:(NSString *)token
success:(void (^)(void))success
failure:(void (^)(NSError *error))failure
{
// Sanity Check
if (_validationState == MXK3PIDAuthStateTokenReceived)
{
if (self.submitUrl)
{
_validationState = MXK3PIDAuthStateTokenSubmitted;
currentRequest = [self submitMsisdnTokenOtherUrl:self.submitUrl token:token medium:self.medium clientSecret:self.clientSecret sid:self.sid success:^{
self->_validationState = MXK3PIDAuthStateAuthenticated;
self->currentRequest = nil;
if (success)
{
success();
}
} failure:^(NSError *error) {
// Return in previous state
self->_validationState = MXK3PIDAuthStateTokenReceived;
self->currentRequest = nil;
if (failure)
{
failure (error);
}
}];
}
else if (self.identityService)
{
_validationState = MXK3PIDAuthStateTokenSubmitted;
currentRequest = [self.identityService submit3PIDValidationToken:token medium:self.medium clientSecret:self.clientSecret sid:self.sid success:^{
self->_validationState = MXK3PIDAuthStateAuthenticated;
self->currentRequest = nil;
if (success)
{
success();
}
} failure:^(NSError *error) {
// Return in previous state
self->_validationState = MXK3PIDAuthStateTokenReceived;
self->currentRequest = nil;
if (failure)
{
failure (error);
}
}];
}
else
{
MXLogDebug(@"[MXK3PID] Failed to submit validation token for 3PID: %@ (%@), identity service is not set", self.address, self.medium);
if (failure)
{
failure(nil);
}
}
}
else
{
MXLogDebug(@"[MXK3PID] Failed to submit validation token for 3PID: %@ (%@), state: %lu", self.address, self.medium, (unsigned long)_validationState);
if (failure)
{
failure(nil);
}
}
}
- (MXHTTPOperation *)submitMsisdnTokenOtherUrl:(NSString *)url
token:(NSString*)token
medium:(NSString *)medium
clientSecret:(NSString *)clientSecret
sid:(NSString *)sid
success:(void (^)(void))success
failure:(void (^)(NSError *))failure
{
NSDictionary *parameters = @{
@"sid": sid,
@"client_secret": clientSecret,
@"token": token
};
MXHTTPClient *httpClient = [[MXHTTPClient alloc] initWithBaseURL:nil andOnUnrecognizedCertificateBlock:nil];
return [httpClient requestWithMethod:@"POST"
path:url
parameters:parameters
success:^(NSDictionary *JSONResponse) {
success();
}
failure:failure];
}
- (void)add3PIDToUser:(BOOL)bind
success:(void (^)(void))success
failure:(void (^)(NSError *error))failure
{
if ([self.medium isEqualToString:kMX3PIDMediumEmail] || [self.medium isEqualToString:kMX3PIDMediumMSISDN])
{
MXWeakify(self);
currentRequest = [mxRestClient add3PID:self.sid clientSecret:self.clientSecret bind:bind success:^{
MXStrongifyAndReturnIfNil(self);
// Update linked userId in 3PID
self.userId = self->mxRestClient.credentials.userId;
self->currentRequest = nil;
if (success)
{
success();
}
} failure:^(NSError *error) {
MXStrongifyAndReturnIfNil(self);
self->currentRequest = nil;
if (failure)
{
failure (error);
}
}];
return;
}
else
{
MXLogDebug(@"[MXK3PID] bindWithUserId: is not supported for this 3PID: %@ (%@)", self.address, self.medium);
}
// Here the validation process failed
if (failure)
{
failure (nil);
}
}
@end
@@ -0,0 +1,290 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <MatrixSDK/MatrixSDK.h>
typedef NS_ENUM(NSUInteger, MXKKeyPreSharingStrategy)
{
MXKKeyPreSharingNone = 0,
MXKKeyPreSharingWhenEnteringRoom = 1,
MXKKeyPreSharingWhenTyping = 2
};
/**
`MXKAppSettings` represents the application settings. Most of them are used to handle matrix session data.
The shared object `standardAppSettings` provides the default application settings defined in `standardUserDefaults`.
Any property change of this shared settings is reported into `standardUserDefaults`.
Developper may define their own `MXKAppSettings` instances to handle specific setting values without impacting the shared object.
*/
@interface MXKAppSettings : NSObject
#pragma mark - /sync filter
/**
Lazy load room members when /syncing with the homeserver.
*/
@property (nonatomic) BOOL syncWithLazyLoadOfRoomMembers;
#pragma mark - Room display
/**
Display all received events in room history (Only recognized events are displayed, presently `custom` events are ignored).
This boolean value is defined in shared settings object with the key: `showAllEventsInRoomHistory`.
Return NO if no value is defined.
*/
@property (nonatomic) BOOL showAllEventsInRoomHistory;
/**
The types of events allowed to be displayed in room history.
Its value depends on `showAllEventsInRoomHistory`.
*/
@property (nonatomic, readonly) NSArray<MXEventTypeString> *eventsFilterForMessages;
/**
All the event types which may be displayed in the room history.
*/
@property (nonatomic, readonly) NSArray<MXEventTypeString> *allEventTypesForMessages;
/**
An allow list for the types of events allowed to be displayed as the last message.
When `nil`, there is no list and all events are allowed.
*/
@property (nonatomic, readonly) NSArray<MXEventTypeString> *lastMessageEventTypesAllowList;
/**
Add event types to `eventsFilterForMessages` and `eventsFilterForMessages`.
@param eventTypes the event types to add.
*/
- (void)addSupportedEventTypes:(NSArray<MXEventTypeString> *)eventTypes;
/**
Remove event types from `eventsFilterForMessages` and `eventsFilterForMessages`.
@param eventTypes the event types to remove.
*/
- (void)removeSupportedEventTypes:(NSArray<MXEventTypeString> *)eventTypes;
/**
Display redacted events in room history.
This boolean value is defined in shared settings object with the key: `showRedactionsInRoomHistory`.
Return NO if no value is defined.
*/
@property (nonatomic) BOOL showRedactionsInRoomHistory;
/**
Display unsupported/unexpected events in room history.
This boolean value is defined in shared settings object with the key: `showUnsupportedEventsInRoomHistory`.
Return NO if no value is defined.
*/
@property (nonatomic) BOOL showUnsupportedEventsInRoomHistory;
/**
Scheme with which to open HTTP links. e.g. if this is set to "googlechrome", any http:// links displayed in a room will be rewritten to use the googlechrome:// scheme.
Defaults to "http".
*/
@property (nonatomic) NSString *httpLinkScheme;
/**
Scheme with which to open HTTPS links. e.g. if this is set to "googlechromes", any https:// links displayed in a room will be rewritten to use the googlechromes:// scheme.
Defaults to "https".
*/
@property (nonatomic) NSString *httpsLinkScheme;
/**
Whether a bubble component should detect the first link in its event's body, storing it in the `link` property.
This boolean value is defined in shared settings object with the key: `enableBubbleComponentLinkDetection`.
Return NO if no value is defined.
*/
@property (nonatomic) BOOL enableBubbleComponentLinkDetection;
/**
Any hosts that should be ignored when calling `mxk_firstURLDetected` on an `NSString` without passing in any parameters.
Customising this value modifies the behaviour of link detection in `MXKRoomBubbleComponent`.
This boolean value is defined in shared settings object with the key: `firstURLDetectionIgnoredHosts`.
The default value of this property only contains the matrix.to host.
*/
@property (nonatomic) NSArray<NSString *> *firstURLDetectionIgnoredHosts;
/**
Indicate to hide un-decryptable events before joining the room. Default is `NO`.
*/
@property (nonatomic) BOOL hidePreJoinedUndecryptableEvents;
/**
Indicate to hide un-decryptable events in the room. Default is `NO`.
*/
@property (nonatomic) BOOL hideUndecryptableEvents;
/**
Indicates the strategy for sharing the outbound session key to other devices of the room
*/
@property (nonatomic) MXKKeyPreSharingStrategy outboundGroupSessionKeyPreSharingStrategy;
#pragma mark - Room members
/**
Sort room members by considering their presence.
Set NO to sort members in alphabetic order.
This boolean value is defined in shared settings object with the key: `sortRoomMembersUsingLastSeenTime`.
Return YES if no value is defined.
*/
@property (nonatomic) BOOL sortRoomMembersUsingLastSeenTime;
/**
Show left members in room member list.
This boolean value is defined in shared settings object with the key: `showLeftMembersInRoomMemberList`.
Return NO if no value is defined.
*/
@property (nonatomic) BOOL showLeftMembersInRoomMemberList;
/// Flag to allow sharing a message or not. Default value is YES.
@property (nonatomic) BOOL messageDetailsAllowSharing;
/// Flag to allow saving a message or not. Default value is YES.
@property (nonatomic) BOOL messageDetailsAllowSaving;
/// Flag to allow copying a media/file or not. Default value is YES.
@property (nonatomic) BOOL messageDetailsAllowCopyingMedia;
/// Flag to allow pasting a media/file or not. Default value is YES.
@property (nonatomic) BOOL messageDetailsAllowPastingMedia;
#pragma mark - Contacts
/**
Return YES if the user allows the local contacts sync.
This boolean value is defined in shared settings object with the key: `syncLocalContacts`.
Return NO if no value is defined.
*/
@property (nonatomic) BOOL syncLocalContacts;
/**
Return YES if the user has been already asked for local contacts sync permission.
This boolean value is defined in shared settings object with the key: `syncLocalContactsPermissionRequested`.
Return NO if no value is defined.
*/
@property (nonatomic) BOOL syncLocalContactsPermissionRequested;
/**
Return YES if after the user has been asked for local contacts sync permission and choose to open
the system's Settings app to enable contacts access.
This boolean value is defined in shared settings object with the key: `syncLocalContactsPermissionOpenedSystemSettings`.
Return NO if no value is defined.
*/
@property (nonatomic) BOOL syncLocalContactsPermissionOpenedSystemSettings;
/**
The current selected country code for the phonebook.
This value is defined in shared settings object with the key: `phonebookCountryCode`.
Return the SIM card information (if any) if no default value is defined.
*/
@property (nonatomic) NSString* phonebookCountryCode;
#pragma mark - Matrix users
/**
Color associated to online matrix users.
This color value is defined in shared settings object with the key: `presenceColorForOnlineUser`.
The default color is `[UIColor greenColor]`.
*/
@property (nonatomic) UIColor *presenceColorForOnlineUser;
/**
Color associated to unavailable matrix users.
This color value is defined in shared settings object with the key: `presenceColorForUnavailableUser`.
The default color is `[UIColor yellowColor]`.
*/
@property (nonatomic) UIColor *presenceColorForUnavailableUser;
/**
Color associated to offline matrix users.
This color value is defined in shared settings object with the key: `presenceColorForOfflineUser`.
The default color is `[UIColor redColor]`.
*/
@property (nonatomic) UIColor *presenceColorForOfflineUser;
#pragma mark - Notifications
/// Flag to allow PushKit pushers or not. Default value is `NO`.
@property (nonatomic, assign) BOOL allowPushKitPushers;
/**
A localization key used when registering the default notification payload.
This key will be translated and displayed for APNS notifications as the body
content, unless it is modified locally by a Notification Service Extension.
The default value for this setting is "MESSAGE". Changes are *not* persisted.
Updating the value after MXKAccount has called `enableAPNSPusher:success:failure:`
will have no effect.
*/
@property (nonatomic) NSString *notificationBodyLocalizationKey;
#pragma mark - Calls
/**
Return YES if the user enable CallKit support.
This boolean value is defined in shared settings object with the key: `enableCallKit`.
Return YES if no value is defined.
*/
@property (nonatomic, getter=isCallKitEnabled) BOOL enableCallKit;
#pragma mark - Shared userDefaults
/**
A userDefaults object that is shared within the application group. The application group identifier
is retrieved from MXSDKOptions sharedInstance (see `applicationGroupIdentifier` property).
The default group is "group.org.matrix".
*/
@property (nonatomic, readonly) NSUserDefaults *sharedUserDefaults;
#pragma mark - Class methods
/**
Return the shared application settings object. These settings are retrieved/stored in the shared defaults object (`[NSUserDefaults standardUserDefaults]`).
*/
+ (MXKAppSettings *)standardAppSettings;
/**
Return the folder to use for caching MatrixKit data.
*/
+ (NSString*)cacheFolder;
/**
Restore the default values.
*/
- (void)reset;
@end
@@ -0,0 +1,865 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKAppSettings.h"
#import "MXKTools.h"
// get ISO country name
#import <CoreTelephony/CTCarrier.h>
#import <CoreTelephony/CTTelephonyNetworkInfo.h>
static MXKAppSettings *standardAppSettings = nil;
static NSString *const kMXAppGroupID = @"group.org.matrix";
@interface MXKAppSettings ()
{
NSMutableArray <NSString*> *eventsFilterForMessages;
NSMutableArray <NSString*> *allEventTypesForMessages;
NSMutableArray <NSString*> *lastMessageEventTypesAllowList;
}
@property (nonatomic, readwrite) NSUserDefaults *sharedUserDefaults;
@property (nonatomic) NSString *currentApplicationGroup;
@end
@implementation MXKAppSettings
@synthesize syncWithLazyLoadOfRoomMembers;
@synthesize showAllEventsInRoomHistory, showRedactionsInRoomHistory, showUnsupportedEventsInRoomHistory, httpLinkScheme, httpsLinkScheme;
@synthesize enableBubbleComponentLinkDetection, firstURLDetectionIgnoredHosts, showLeftMembersInRoomMemberList, sortRoomMembersUsingLastSeenTime;
@synthesize syncLocalContacts, syncLocalContactsPermissionRequested, syncLocalContactsPermissionOpenedSystemSettings, phonebookCountryCode;
@synthesize presenceColorForOnlineUser, presenceColorForUnavailableUser, presenceColorForOfflineUser;
@synthesize enableCallKit;
@synthesize sharedUserDefaults;
+ (MXKAppSettings *)standardAppSettings
{
@synchronized(self)
{
if(standardAppSettings == nil)
{
standardAppSettings = [[super allocWithZone:NULL] init];
}
}
return standardAppSettings;
}
+ (NSString *)cacheFolder
{
NSString *cacheFolder;
// Check for a potential application group id
NSString *applicationGroupIdentifier = [MXSDKOptions sharedInstance].applicationGroupIdentifier;
if (applicationGroupIdentifier)
{
NSURL *sharedContainerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:applicationGroupIdentifier];
cacheFolder = [sharedContainerURL path];
}
else
{
NSArray *cacheDirList = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
cacheFolder = [cacheDirList objectAtIndex:0];
}
// Use a dedicated cache folder for MatrixKit
cacheFolder = [cacheFolder stringByAppendingPathComponent:@"MatrixKit"];
// Make sure the folder exists so that it can be used
if (cacheFolder && ![[NSFileManager defaultManager] fileExistsAtPath:cacheFolder])
{
NSError *error;
[[NSFileManager defaultManager] createDirectoryAtPath:cacheFolder withIntermediateDirectories:YES attributes:nil error:&error];
if (error)
{
MXLogDebug(@"[MXKAppSettings] cacheFolder: Error: Cannot create MatrixKit folder at %@. Error: %@", cacheFolder, error);
}
}
return cacheFolder;
}
#pragma mark -
-(instancetype)init
{
if (self = [super init])
{
syncWithLazyLoadOfRoomMembers = YES;
// Use presence to sort room members by default
if (![[NSUserDefaults standardUserDefaults] objectForKey:@"sortRoomMembersUsingLastSeenTime"])
{
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"sortRoomMembersUsingLastSeenTime"];
}
_hidePreJoinedUndecryptableEvents = NO;
_hideUndecryptableEvents = NO;
sortRoomMembersUsingLastSeenTime = YES;
presenceColorForOnlineUser = [UIColor greenColor];
presenceColorForUnavailableUser = [UIColor yellowColor];
presenceColorForOfflineUser = [UIColor redColor];
httpLinkScheme = @"http";
httpsLinkScheme = @"https";
enableBubbleComponentLinkDetection = NO;
firstURLDetectionIgnoredHosts = @[[NSURL URLWithString:kMXMatrixDotToUrl].host];
_allowPushKitPushers = NO;
_notificationBodyLocalizationKey = @"MESSAGE";
enableCallKit = YES;
eventsFilterForMessages = @[
kMXEventTypeStringRoomCreate,
kMXEventTypeStringRoomName,
kMXEventTypeStringRoomTopic,
kMXEventTypeStringRoomMember,
kMXEventTypeStringRoomEncrypted,
kMXEventTypeStringRoomEncryption,
kMXEventTypeStringRoomHistoryVisibility,
kMXEventTypeStringRoomMessage,
kMXEventTypeStringRoomThirdPartyInvite,
kMXEventTypeStringRoomGuestAccess,
kMXEventTypeStringRoomJoinRules,
kMXEventTypeStringCallInvite,
kMXEventTypeStringCallAnswer,
kMXEventTypeStringCallHangup,
kMXEventTypeStringCallReject,
kMXEventTypeStringCallNegotiate,
kMXEventTypeStringSticker,
kMXEventTypeStringKeyVerificationCancel,
kMXEventTypeStringKeyVerificationDone
].mutableCopy;
// List all the event types, except kMXEventTypeStringPresence which are not related to a specific room.
allEventTypesForMessages = @[
kMXEventTypeStringRoomName,
kMXEventTypeStringRoomTopic,
kMXEventTypeStringRoomMember,
kMXEventTypeStringRoomCreate,
kMXEventTypeStringRoomEncrypted,
kMXEventTypeStringRoomEncryption,
kMXEventTypeStringRoomJoinRules,
kMXEventTypeStringRoomPowerLevels,
kMXEventTypeStringRoomAliases,
kMXEventTypeStringRoomHistoryVisibility,
kMXEventTypeStringRoomMessage,
kMXEventTypeStringRoomMessageFeedback,
kMXEventTypeStringRoomRedaction,
kMXEventTypeStringRoomThirdPartyInvite,
kMXEventTypeStringRoomRelatedGroups,
kMXEventTypeStringReaction,
kMXEventTypeStringCallInvite,
kMXEventTypeStringCallAnswer,
kMXEventTypeStringCallSelectAnswer,
kMXEventTypeStringCallHangup,
kMXEventTypeStringCallReject,
kMXEventTypeStringCallNegotiate,
kMXEventTypeStringSticker,
kMXEventTypeStringKeyVerificationCancel,
kMXEventTypeStringKeyVerificationDone
].mutableCopy;
lastMessageEventTypesAllowList = @[
kMXEventTypeStringRoomCreate, // Without any messages, calls or stickers an event is needed to provide a date.
kMXEventTypeStringRoomEncrypted, // Show a UTD string rather than the previous message.
kMXEventTypeStringRoomMessage,
kMXEventTypeStringRoomMember,
kMXEventTypeStringCallInvite,
kMXEventTypeStringCallAnswer,
kMXEventTypeStringCallHangup,
kMXEventTypeStringSticker
].mutableCopy;
_messageDetailsAllowSharing = YES;
_messageDetailsAllowSaving = YES;
_messageDetailsAllowCopyingMedia = YES;
_messageDetailsAllowPastingMedia = YES;
_outboundGroupSessionKeyPreSharingStrategy = MXKKeyPreSharingWhenTyping;
}
return self;
}
- (void)reset
{
if (self == [MXKAppSettings standardAppSettings])
{
// Flush shared user defaults
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"syncWithLazyLoadOfRoomMembers2"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"showAllEventsInRoomHistory"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"showRedactionsInRoomHistory"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"showUnsupportedEventsInRoomHistory"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"sortRoomMembersUsingLastSeenTime"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"showLeftMembersInRoomMemberList"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"syncLocalContactsPermissionRequested"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"syncLocalContacts"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"phonebookCountryCode"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"presenceColorForOnlineUser"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"presenceColorForUnavailableUser"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"presenceColorForOfflineUser"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"httpLinkScheme"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"httpsLinkScheme"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"enableBubbleComponentLinkDetection"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"firstURLDetectionIgnoredHosts"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"enableCallKit"];
}
else
{
syncWithLazyLoadOfRoomMembers = YES;
showAllEventsInRoomHistory = NO;
showRedactionsInRoomHistory = NO;
showUnsupportedEventsInRoomHistory = NO;
sortRoomMembersUsingLastSeenTime = YES;
showLeftMembersInRoomMemberList = NO;
syncLocalContactsPermissionRequested = NO;
syncLocalContacts = NO;
phonebookCountryCode = nil;
presenceColorForOnlineUser = [UIColor greenColor];
presenceColorForUnavailableUser = [UIColor yellowColor];
presenceColorForOfflineUser = [UIColor redColor];
httpLinkScheme = @"http";
httpsLinkScheme = @"https";
enableCallKit = YES;
}
}
- (NSUserDefaults *)sharedUserDefaults
{
if (sharedUserDefaults)
{
// Check whether the current group id did not change.
NSString *applicationGroup = [MXSDKOptions sharedInstance].applicationGroupIdentifier;
if (!applicationGroup.length)
{
applicationGroup = kMXAppGroupID;
}
if (![_currentApplicationGroup isEqualToString:applicationGroup])
{
// Reset the existing shared object
sharedUserDefaults = nil;
}
}
if (!sharedUserDefaults)
{
_currentApplicationGroup = [MXSDKOptions sharedInstance].applicationGroupIdentifier;
if (!_currentApplicationGroup.length)
{
_currentApplicationGroup = kMXAppGroupID;
}
sharedUserDefaults = [[NSUserDefaults alloc] initWithSuiteName:_currentApplicationGroup];
}
return sharedUserDefaults;
}
#pragma mark - Calls
- (BOOL)syncWithLazyLoadOfRoomMembers
{
if (self == [MXKAppSettings standardAppSettings])
{
id storedValue = [[NSUserDefaults standardUserDefaults] objectForKey:@"syncWithLazyLoadOfRoomMembers2"];
if (storedValue)
{
return [(NSNumber *)storedValue boolValue];
}
else
{
// Enabled by default
return YES;
}
}
else
{
return syncWithLazyLoadOfRoomMembers;
}
}
- (void)setSyncWithLazyLoadOfRoomMembers:(BOOL)syncWithLazyLoadOfRoomMembers
{
if (self == [MXKAppSettings standardAppSettings])
{
[[NSUserDefaults standardUserDefaults] setBool:syncWithLazyLoadOfRoomMembers forKey:@"syncWithLazyLoadOfRoomMembers2"];
}
else
{
syncWithLazyLoadOfRoomMembers = syncWithLazyLoadOfRoomMembers;
}
}
#pragma mark - Room display
- (BOOL)showAllEventsInRoomHistory
{
if (self == [MXKAppSettings standardAppSettings])
{
return [[NSUserDefaults standardUserDefaults] boolForKey:@"showAllEventsInRoomHistory"];
}
else
{
return showAllEventsInRoomHistory;
}
}
- (void)setShowAllEventsInRoomHistory:(BOOL)boolValue
{
if (self == [MXKAppSettings standardAppSettings])
{
[[NSUserDefaults standardUserDefaults] setBool:boolValue forKey:@"showAllEventsInRoomHistory"];
}
else
{
showAllEventsInRoomHistory = boolValue;
}
}
- (NSArray *)eventsFilterForMessages
{
if (showAllEventsInRoomHistory)
{
// Consider all the event types
return self.allEventTypesForMessages;
}
else
{
// Display only a subset of events
return eventsFilterForMessages;
}
}
- (NSArray *)allEventTypesForMessages
{
return allEventTypesForMessages;
}
- (NSArray<MXEventTypeString> *)lastMessageEventTypesAllowList
{
return lastMessageEventTypesAllowList;
}
- (void)addSupportedEventTypes:(NSArray<NSString *> *)eventTypes
{
[eventsFilterForMessages addObjectsFromArray:eventTypes];
[allEventTypesForMessages addObjectsFromArray:eventTypes];
}
- (void)removeSupportedEventTypes:(NSArray<NSString *> *)eventTypes
{
[eventsFilterForMessages removeObjectsInArray:eventTypes];
[allEventTypesForMessages removeObjectsInArray:eventTypes];
[lastMessageEventTypesAllowList removeObjectsInArray:eventTypes];
}
- (BOOL)showRedactionsInRoomHistory
{
if (self == [MXKAppSettings standardAppSettings])
{
return [[NSUserDefaults standardUserDefaults] boolForKey:@"showRedactionsInRoomHistory"];
}
else
{
return showRedactionsInRoomHistory;
}
}
- (void)setShowRedactionsInRoomHistory:(BOOL)boolValue
{
if (self == [MXKAppSettings standardAppSettings])
{
[[NSUserDefaults standardUserDefaults] setBool:boolValue forKey:@"showRedactionsInRoomHistory"];
}
else
{
showRedactionsInRoomHistory = boolValue;
}
}
- (BOOL)showUnsupportedEventsInRoomHistory
{
if (self == [MXKAppSettings standardAppSettings])
{
return [[NSUserDefaults standardUserDefaults] boolForKey:@"showUnsupportedEventsInRoomHistory"];
}
else
{
return showUnsupportedEventsInRoomHistory;
}
}
- (void)setShowUnsupportedEventsInRoomHistory:(BOOL)boolValue
{
if (self == [MXKAppSettings standardAppSettings])
{
[[NSUserDefaults standardUserDefaults] setBool:boolValue forKey:@"showUnsupportedEventsInRoomHistory"];
}
else
{
showUnsupportedEventsInRoomHistory = boolValue;
}
}
- (NSString *)httpLinkScheme
{
if (self == [MXKAppSettings standardAppSettings])
{
NSString *ret = [[NSUserDefaults standardUserDefaults] stringForKey:@"httpLinkScheme"];
if (ret == nil) {
ret = @"http";
}
return ret;
}
else
{
return httpLinkScheme;
}
}
- (void)setHttpLinkScheme:(NSString *)stringValue
{
if (self == [MXKAppSettings standardAppSettings])
{
[[NSUserDefaults standardUserDefaults] setObject:stringValue forKey:@"httpLinkScheme"];
}
else
{
httpLinkScheme = stringValue;
}
}
- (NSString *)httpsLinkScheme
{
if (self == [MXKAppSettings standardAppSettings])
{
NSString *ret = [[NSUserDefaults standardUserDefaults] stringForKey:@"httpsLinkScheme"];
if (ret == nil) {
ret = @"https";
}
return ret;
}
else
{
return httpsLinkScheme;
}
}
- (void)setHttpsLinkScheme:(NSString *)stringValue
{
if (self == [MXKAppSettings standardAppSettings])
{
[[NSUserDefaults standardUserDefaults] setObject:stringValue forKey:@"httpsLinkScheme"];
}
else
{
httpsLinkScheme = stringValue;
}
}
- (BOOL)enableBubbleComponentLinkDetection
{
if (self == [MXKAppSettings standardAppSettings])
{
return [NSUserDefaults.standardUserDefaults boolForKey:@"enableBubbleComponentLinkDetection"];
}
else
{
return enableBubbleComponentLinkDetection;
}
}
- (void)setEnableBubbleComponentLinkDetection:(BOOL)storeLinksInBubbleComponents
{
if (self == [MXKAppSettings standardAppSettings])
{
[NSUserDefaults.standardUserDefaults setBool:storeLinksInBubbleComponents forKey:@"enableBubbleComponentLinkDetection"];
}
else
{
enableBubbleComponentLinkDetection = storeLinksInBubbleComponents;
}
}
- (NSArray<NSString *> *)firstURLDetectionIgnoredHosts
{
if (self == [MXKAppSettings standardAppSettings])
{
return [NSUserDefaults.standardUserDefaults objectForKey:@"firstURLDetectionIgnoredHosts"] ?: @[[NSURL URLWithString:kMXMatrixDotToUrl].host];
}
else
{
return firstURLDetectionIgnoredHosts;
}
}
- (void)setFirstURLDetectionIgnoredHosts:(NSArray<NSString *> *)ignoredHosts
{
if (self == [MXKAppSettings standardAppSettings])
{
if (ignoredHosts == nil)
{
ignoredHosts = @[];
}
[NSUserDefaults.standardUserDefaults setObject:ignoredHosts forKey:@"firstURLDetectionIgnoredHosts"];
}
else
{
firstURLDetectionIgnoredHosts = ignoredHosts;
}
}
#pragma mark - Room members
- (BOOL)sortRoomMembersUsingLastSeenTime
{
if (self == [MXKAppSettings standardAppSettings])
{
return [[NSUserDefaults standardUserDefaults] boolForKey:@"sortRoomMembersUsingLastSeenTime"];
}
else
{
return sortRoomMembersUsingLastSeenTime;
}
}
- (void)setSortRoomMembersUsingLastSeenTime:(BOOL)boolValue
{
if (self == [MXKAppSettings standardAppSettings])
{
[[NSUserDefaults standardUserDefaults] setBool:boolValue forKey:@"sortRoomMembersUsingLastSeenTime"];
}
else
{
sortRoomMembersUsingLastSeenTime = boolValue;
}
}
- (BOOL)showLeftMembersInRoomMemberList
{
if (self == [MXKAppSettings standardAppSettings])
{
return [[NSUserDefaults standardUserDefaults] boolForKey:@"showLeftMembersInRoomMemberList"];
}
else
{
return showLeftMembersInRoomMemberList;
}
}
- (void)setShowLeftMembersInRoomMemberList:(BOOL)boolValue
{
if (self == [MXKAppSettings standardAppSettings])
{
[[NSUserDefaults standardUserDefaults] setBool:boolValue forKey:@"showLeftMembersInRoomMemberList"];
}
else
{
showLeftMembersInRoomMemberList = boolValue;
}
}
#pragma mark - Contacts
- (BOOL)syncLocalContacts
{
if (self == [MXKAppSettings standardAppSettings])
{
return [[NSUserDefaults standardUserDefaults] boolForKey:@"syncLocalContacts"];
}
else
{
return syncLocalContacts;
}
}
- (void)setSyncLocalContacts:(BOOL)boolValue
{
if (self == [MXKAppSettings standardAppSettings])
{
[[NSUserDefaults standardUserDefaults] setBool:boolValue forKey:@"syncLocalContacts"];
}
else
{
syncLocalContacts = boolValue;
}
}
- (BOOL)syncLocalContactsPermissionRequested
{
if (self == [MXKAppSettings standardAppSettings])
{
return [[NSUserDefaults standardUserDefaults] boolForKey:@"syncLocalContactsPermissionRequested"];
}
else
{
return syncLocalContactsPermissionRequested;
}
}
- (void)setSyncLocalContactsPermissionRequested:(BOOL)theSyncLocalContactsPermissionRequested
{
if (self == [MXKAppSettings standardAppSettings])
{
[[NSUserDefaults standardUserDefaults] setBool:theSyncLocalContactsPermissionRequested forKey:@"syncLocalContactsPermissionRequested"];
}
else
{
syncLocalContactsPermissionRequested = theSyncLocalContactsPermissionRequested;
}
}
- (BOOL)syncLocalContactsPermissionOpenedSystemSettings
{
if (self == [MXKAppSettings standardAppSettings])
{
return [[NSUserDefaults standardUserDefaults] boolForKey:@"syncLocalContactsPermissionOpenedSystemSettings"];
}
else
{
return syncLocalContactsPermissionOpenedSystemSettings;
}
}
- (void)setSyncLocalContactsPermissionOpenedSystemSettings:(BOOL)theSyncLocalContactsPermissionOpenedSystemSettings
{
if (self == [MXKAppSettings standardAppSettings])
{
[[NSUserDefaults standardUserDefaults] setBool:theSyncLocalContactsPermissionOpenedSystemSettings forKey:@"syncLocalContactsPermissionOpenedSystemSettings"];
}
else
{
syncLocalContactsPermissionOpenedSystemSettings = theSyncLocalContactsPermissionOpenedSystemSettings;
}
}
- (NSString*)phonebookCountryCode
{
NSString* res = phonebookCountryCode;
if (self == [MXKAppSettings standardAppSettings])
{
res = [[NSUserDefaults standardUserDefaults] stringForKey:@"phonebookCountryCode"];
}
// does not exist : try to get the SIM card information
if (!res)
{
// get the current MCC
CTTelephonyNetworkInfo *netInfo = [[CTTelephonyNetworkInfo alloc] init];
CTCarrier *carrier = [netInfo subscriberCellularProvider];
if (carrier)
{
res = [[carrier isoCountryCode] uppercaseString];
if (res)
{
[self setPhonebookCountryCode:res];
}
}
}
return res;
}
- (void)setPhonebookCountryCode:(NSString *)stringValue
{
if (self == [MXKAppSettings standardAppSettings])
{
[[NSUserDefaults standardUserDefaults] setObject:stringValue forKey:@"phonebookCountryCode"];
}
else
{
phonebookCountryCode = stringValue;
}
}
#pragma mark - Matrix users
- (UIColor*)presenceColorForOnlineUser
{
UIColor *color = presenceColorForOnlineUser;
if (self == [MXKAppSettings standardAppSettings])
{
NSNumber *rgbValue = [[NSUserDefaults standardUserDefaults] objectForKey:@"presenceColorForOnlineUser"];
if (rgbValue)
{
color = [MXKTools colorWithRGBValue:[rgbValue unsignedIntegerValue]];
}
else
{
color = [UIColor greenColor];
}
}
return color;
}
- (void)setPresenceColorForOnlineUser:(UIColor*)color
{
if (self == [MXKAppSettings standardAppSettings])
{
if (color)
{
NSUInteger rgbValue = [MXKTools rgbValueWithColor:color];
[[NSUserDefaults standardUserDefaults] setInteger:rgbValue forKey:@"presenceColorForOnlineUser"];
}
else
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"presenceColorForOnlineUser"];
}
}
else
{
presenceColorForOnlineUser = color ? color : [UIColor greenColor];
}
}
- (UIColor*)presenceColorForUnavailableUser
{
UIColor *color = presenceColorForUnavailableUser;
if (self == [MXKAppSettings standardAppSettings])
{
NSNumber *rgbValue = [[NSUserDefaults standardUserDefaults] objectForKey:@"presenceColorForUnavailableUser"];
if (rgbValue)
{
color = [MXKTools colorWithRGBValue:[rgbValue unsignedIntegerValue]];
}
else
{
color = [UIColor yellowColor];
}
}
return color;
}
- (void)setPresenceColorForUnavailableUser:(UIColor*)color
{
if (self == [MXKAppSettings standardAppSettings])
{
if (color)
{
NSUInteger rgbValue = [MXKTools rgbValueWithColor:color];
[[NSUserDefaults standardUserDefaults] setInteger:rgbValue forKey:@"presenceColorForUnavailableUser"];
}
else
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"presenceColorForUnavailableUser"];
}
}
else
{
presenceColorForUnavailableUser = color ? color : [UIColor yellowColor];
}
}
- (UIColor*)presenceColorForOfflineUser
{
UIColor *color = presenceColorForOfflineUser;
if (self == [MXKAppSettings standardAppSettings])
{
NSNumber *rgbValue = [[NSUserDefaults standardUserDefaults] objectForKey:@"presenceColorForOfflineUser"];
if (rgbValue)
{
color = [MXKTools colorWithRGBValue:[rgbValue unsignedIntegerValue]];
}
else
{
color = [UIColor redColor];
}
}
return color;
}
- (void)setPresenceColorForOfflineUser:(UIColor *)color
{
if (self == [MXKAppSettings standardAppSettings])
{
if (color)
{
NSUInteger rgbValue = [MXKTools rgbValueWithColor:color];
[[NSUserDefaults standardUserDefaults] setInteger:rgbValue forKey:@"presenceColorForOfflineUser"];
}
else
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"presenceColorForOfflineUser"];
}
}
else
{
presenceColorForOfflineUser = color ? color : [UIColor redColor];
}
}
#pragma mark - Calls
- (BOOL)isCallKitEnabled
{
if (self == [MXKAppSettings standardAppSettings])
{
id storedValue = [[NSUserDefaults standardUserDefaults] objectForKey:@"enableCallKit"];
if (storedValue)
{
return [(NSNumber *)storedValue boolValue];
}
else
{
return YES;
}
}
else
{
return enableCallKit;
}
}
- (void)setEnableCallKit:(BOOL)enable
{
if (self == [MXKAppSettings standardAppSettings])
{
[[NSUserDefaults standardUserDefaults] setBool:enable forKey:@"enableCallKit"];
}
else
{
enableCallKit = enable;
}
}
@end
@@ -0,0 +1,27 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
/**
`MXKCellData` objects contain data that is displayed by objects implementing `MXKCellRendering`.
The goal of `MXKCellData` is mainly to cache computed data in order to avoid to compute it each time
a cell is displayed.
*/
@interface MXKCellData : NSObject
@end
@@ -0,0 +1,21 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKCellData.h"
@implementation MXKCellData
@end
@@ -0,0 +1,225 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <MatrixSDK/MatrixSDK.h>
#import "MXKCellRendering.h"
/**
List data source states.
*/
typedef enum : NSUInteger {
/**
Default value (used when all resources have been disposed).
The instance cannot be used anymore.
*/
MXKDataSourceStateUnknown,
/**
Initialisation is in progress.
*/
MXKDataSourceStatePreparing,
/**
Something wrong happens during initialisation.
*/
MXKDataSourceStateFailed,
/**
Data source is ready to be used.
*/
MXKDataSourceStateReady
} MXKDataSourceState;
@protocol MXKDataSourceDelegate;
/**
`MXKDataSource` is the base class for data sources managed by MatrixKit.
Inherited 'MXKDataSource' instances are used to handle table or collection data.
They may conform to UITableViewDataSource or UICollectionViewDataSource protocol to be used as data source delegate
for a UITableView or a UICollectionView instance.
*/
@interface MXKDataSource : NSObject <MXKCellRenderingDelegate>
{
@protected
MXKDataSourceState state;
}
/**
The matrix session.
*/
@property (nonatomic, weak, readonly) MXSession *mxSession;
/**
The data source state
*/
@property (nonatomic, readonly) MXKDataSourceState state;
/**
The delegate notified when the data has been updated.
*/
@property (weak, nonatomic) id<MXKDataSourceDelegate> delegate;
#pragma mark - Life cycle
/**
Base constructor of data source.
Customization like class registrations must be done before loading data (see '[MXKDataSource registerCellDataClass: forCellIdentifier:]') .
That is why 3 steps should be considered during 'MXKDataSource' initialization:
1- call [MXKDataSource initWithMatrixSession:] to initialize a new allocated object.
2- customize classes and others...
3- call [MXKDataSource finalizeInitialization] to finalize the initialization.
@param mxSession the Matrix session to get data from.
@return the newly created instance.
*/
- (instancetype)initWithMatrixSession:(MXSession*)mxSession;
/**
Finalize the initialization by adding an observer on matrix session state change.
*/
- (void)finalizeInitialization;
/**
Dispose all resources.
*/
- (void)destroy;
/**
This method is called when the state of the attached Matrix session has changed.
*/
- (void)didMXSessionStateChange;
#pragma mark - MXKCellData classes
/**
Register the MXKCellData class that will be used to process and store data for cells
with the designated identifier.
@param cellDataClass a MXKCellData-inherited class that will handle data for cells.
@param identifier the identifier of targeted cell.
*/
- (void)registerCellDataClass:(Class)cellDataClass forCellIdentifier:(NSString *)identifier;
/**
Return the MXKCellData class that handles data for cells with the designated identifier.
@param identifier the cell identifier.
@return the associated MXKCellData-inherited class.
*/
- (Class)cellDataClassForCellIdentifier:(NSString *)identifier;
#pragma mark - Pending HTTP requests
/**
Cancel all registered requests.
*/
- (void)cancelAllRequests;
@end
@protocol MXKDataSourceDelegate <NSObject>
/**
Ask the delegate which MXKCellRendering-compliant class must be used to render this cell data.
This method is called when MXKDataSource instance is used as the data source delegate of a table or a collection.
CAUTION: The table or the collection MUST have registered the returned class with the same identifier than the one returned by [cellReuseIdentifierForCellData:].
@param cellData the cell data to display.
@return a MXKCellRendering-compliant class which inherits UITableViewCell or UICollectionViewCell class (nil if the cellData is not supported).
*/
- (Class<MXKCellRendering>)cellViewClassForCellData:(MXKCellData*)cellData;
/**
Ask the delegate which identifier must be used to dequeue reusable cell for this cell data.
This method is called when MXKDataSource instance is used as the data source delegate of a table or a collection.
CAUTION: The table or the collection MUST have registered the right class with the returned identifier (see [cellViewClassForCellData:]).
@param cellData the cell data to display.
@return the reuse identifier for the cell (nil if the cellData is not supported).
*/
- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData;
/**
Tells the delegate that some cell data/views have been changed.
@param dataSource the involved data source.
@param changes contains the index paths of objects that changed.
*/
- (void)dataSource:(MXKDataSource*)dataSource didCellChange:(id /* @TODO*/)changes;
@optional
/**
Tells the delegate that data source state changed
@param dataSource the involved data source.
@param state the new data source state.
*/
- (void)dataSource:(MXKDataSource*)dataSource didStateChange:(MXKDataSourceState)state;
/**
Relevant only for data source which support multi-sessions.
Tells the delegate that a matrix session has been added.
@param dataSource the involved data source.
@param mxSession the new added session.
*/
- (void)dataSource:(MXKDataSource*)dataSource didAddMatrixSession:(MXSession*)mxSession;
/**
Relevant only for data source which support multi-sessions.
Tells the delegate that a matrix session has been removed.
@param dataSource the involved data source.
@param mxSession the removed session.
*/
- (void)dataSource:(MXKDataSource*)dataSource didRemoveMatrixSession:(MXSession*)mxSession;
/**
Tells the delegate when a user action is observed inside a cell.
@see `MXKCellRenderingDelegate` for more details.
@param dataSource the involved data source.
@param actionIdentifier an identifier indicating the action type (tap, long press...) and which part of the cell is concerned.
@param cell the cell in which action has been observed.
@param userInfo a dict containing additional information. It depends on actionIdentifier. May be nil.
*/
- (void)dataSource:(MXKDataSource*)dataSource didRecognizeAction:(NSString*)actionIdentifier inCell:(id<MXKCellRendering>)cell userInfo:(NSDictionary*)userInfo;
/**
Asks the delegate if a user action (click on a link) can be done.
@see `MXKCellRenderingDelegate` for more details.
@param dataSource the involved data source.
@param actionIdentifier an identifier indicating the action type (link click) and which part of the cell is concerned.
@param cell the cell in which action has been observed.
@param userInfo a dict containing additional information. It depends on actionIdentifier. May be nil.
@param defaultValue the value to return by default if the action is not handled.
@return a boolean value which depends on actionIdentifier.
*/
- (BOOL)dataSource:(MXKDataSource*)dataSource shouldDoAction:(NSString *)actionIdentifier inCell:(id<MXKCellRendering>)cell userInfo:(NSDictionary *)userInfo defaultValue:(BOOL)defaultValue;
@end
@@ -0,0 +1,148 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKDataSource.h"
#import "MXKCellData.h"
#import "MXKCellRendering.h"
@interface MXKDataSource ()
{
/**
The mapping between cell identifiers and MXKCellData classes.
*/
NSMutableDictionary *cellDataMap;
}
@end
@implementation MXKDataSource
@synthesize state;
#pragma mark - Life cycle
- (instancetype)init
{
self = [super init];
if (self)
{
state = MXKDataSourceStateUnknown;
cellDataMap = [NSMutableDictionary dictionary];
}
return self;
}
- (instancetype)initWithMatrixSession:(MXSession *)matrixSession
{
self = [self init];
if (self)
{
_mxSession = matrixSession;
state = MXKDataSourceStatePreparing;
}
return self;
}
- (void)finalizeInitialization
{
// Add an observer on matrix session state change (prevent multiple registrations).
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionStateDidChangeNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionStateChange:) name:kMXSessionStateDidChangeNotification object:nil];
// Call the registered callback to finalize the initialisation step.
[self didMXSessionStateChange];
}
- (void)destroy
{
state = MXKDataSourceStateUnknown;
if (_delegate && [_delegate respondsToSelector:@selector(dataSource:didStateChange:)])
{
[_delegate dataSource:self didStateChange:state];
}
_mxSession = nil;
_delegate = nil;
[self cancelAllRequests];
[[NSNotificationCenter defaultCenter] removeObserver:self];
cellDataMap = nil;
}
#pragma mark - MXSessionStateDidChangeNotification
- (void)didMXSessionStateChange:(NSNotification *)notif
{
// Check this is our Matrix session that has changed
if (notif.object == _mxSession)
{
[self didMXSessionStateChange];
}
}
- (void)didMXSessionStateChange
{
// The inherited class is highly invited to override this method for its business logic
}
#pragma mark - MXKCellData classes
- (void)registerCellDataClass:(Class)cellDataClass forCellIdentifier:(NSString *)identifier
{
// Sanity check: accept only MXKCellData classes or sub-classes
NSParameterAssert([cellDataClass isSubclassOfClass:MXKCellData.class]);
cellDataMap[identifier] = cellDataClass;
}
- (Class)cellDataClassForCellIdentifier:(NSString *)identifier
{
return cellDataMap[identifier];
}
#pragma mark - MXKCellRenderingDelegate
- (void)cell:(id<MXKCellRendering>)cell didRecognizeAction:(NSString*)actionIdentifier userInfo:(NSDictionary *)userInfo
{
// The data source simply relays the information to its delegate
if (_delegate && [_delegate respondsToSelector:@selector(dataSource:didRecognizeAction:inCell:userInfo:)])
{
[_delegate dataSource:self didRecognizeAction:actionIdentifier inCell:cell userInfo:userInfo];
}
}
- (BOOL)cell:(id<MXKCellRendering>)cell shouldDoAction:(NSString *)actionIdentifier userInfo:(NSDictionary *)userInfo defaultValue:(BOOL)defaultValue
{
BOOL shouldDoAction = defaultValue;
// The data source simply relays the question to its delegate
if (_delegate && [_delegate respondsToSelector:@selector(dataSource:shouldDoAction:inCell:userInfo:defaultValue:)])
{
shouldDoAction = [_delegate dataSource:self shouldDoAction:actionIdentifier inCell:cell userInfo:userInfo defaultValue:defaultValue];
}
return shouldDoAction;
}
#pragma mark - Pending HTTP requests
/**
Cancel all registered requests.
*/
- (void)cancelAllRequests
{
// The inherited class is invited to override this method
}
@end
@@ -0,0 +1,33 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Foundation
import UIKit
@objcMembers
public class MXKPasteboardManager: NSObject {
public static let shared = MXKPasteboardManager(withPasteboard: .general)
private init(withPasteboard pasteboard: UIPasteboard) {
self.pasteboard = pasteboard
super.init()
}
/// Pasteboard to use on copy operations. Defaults to `UIPasteboard.generalPasteboard`.
public var pasteboard: UIPasteboard
}
@@ -0,0 +1,27 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <MatrixSDK/MatrixSDK.h>
#import "MXKDirectoryServerCellDataStoring.h"
/**
`MXKRoomMemberCellData` modelised the data for a `MXKRoomMemberTableViewCell` cell.
*/
@interface MXKDirectoryServerCellData : MXKCellData <MXKDirectoryServerCellDataStoring>
@end
@@ -0,0 +1,66 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKDirectoryServerCellData.h"
#import "NSBundle+MatrixKit.h"
#import "MXKSwiftHeader.h"
@implementation MXKDirectoryServerCellData;
@synthesize desc, icon;
@synthesize homeserver, includeAllNetworks;
@synthesize thirdPartyProtocolInstance, thirdPartyProtocol;
@synthesize mediaManager;
- (id)initWithHomeserver:(NSString *)theHomeserver includeAllNetworks:(BOOL)theIncludeAllNetworks
{
self = [super init];
if (self)
{
homeserver = theHomeserver;
includeAllNetworks = theIncludeAllNetworks;
if (theIncludeAllNetworks)
{
desc = homeserver;
icon = nil;
}
else
{
// Use the Matrix name and logo when looking for Matrix rooms only
desc = [MatrixKitL10n matrix];
icon = [NSBundle mxk_imageFromMXKAssetsBundleWithName:@"network_matrix"];
}
}
return self;
}
- (id)initWithProtocolInstance:(MXThirdPartyProtocolInstance *)instance protocol:(MXThirdPartyProtocol *)protocol
{
self = [super init];
if (self)
{
thirdPartyProtocolInstance = instance;
thirdPartyProtocol = protocol;
desc = thirdPartyProtocolInstance.desc;
icon = nil;
}
return self;
}
@end
@@ -0,0 +1,75 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <MatrixSDK/MatrixSDK.h>
#import "MXKCellData.h"
/**
`MXKDirectoryServerCellDataStoring` defines a protocol a class must conform in order to
store directory cell data managed by `MXKDirectoryServersDataSource`.
*/
@protocol MXKDirectoryServerCellDataStoring <NSObject>
#pragma mark - Data displayed by a server cell
/**
The name of the directory server.
*/
@property (nonatomic) NSString *desc;
/**
The icon of the server.
*/
@property (nonatomic) UIImage *icon;
/**
The optional media manager used to download the icon of the server.
*/
@property (nonatomic) MXMediaManager *mediaManager;
/**
In case the cell data represents a homeserver, its description.
*/
@property (nonatomic, readonly) NSString *homeserver;
@property (nonatomic, readonly) BOOL includeAllNetworks;
/**
In case the cell data represents a third-party protocol instance, its description.
*/
@property (nonatomic, readonly) MXThirdPartyProtocolInstance *thirdPartyProtocolInstance;
@property (nonatomic, readonly) MXThirdPartyProtocol *thirdPartyProtocol;
/**
Define a MXKDirectoryServerCellData that will store a homeserver.
@param homeserver the homeserver name (ex: "matrix.org).
@param includeAllNetworks YES to list all public rooms on the homeserver whatever their protocol.
NO to list only matrix rooms.
*/
- (id)initWithHomeserver:(NSString*)homeserver includeAllNetworks:(BOOL)includeAllNetworks;
/**
Define a MXKDirectoryServerCellData that will store a third-party protocol instance.
@param instance the instance of the protocol.
@param protocol the protocol description.
*/
- (id)initWithProtocolInstance:(MXThirdPartyProtocolInstance*)instance protocol:(MXThirdPartyProtocol*)protocol;
@end
@@ -0,0 +1,82 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "MXKDataSource.h"
#import "MXKDirectoryServerCellDataStoring.h"
/**
Identifier to use for cells that display a server in the servers list.
*/
FOUNDATION_EXPORT NSString *const kMXKDirectorServerCellIdentifier;
/**
`DirectoryServersDataSource` is a base class to list servers and third-party protocols
instances available on the user homeserver.
We can then list public rooms from the directory of these servers. This is done
with `PublicRoomsDirectoryDataSource`.
As a `MXKDataSource` child class, the class has a state where values have the following meanings:
- MXKDataSourceStatePreparing: the data source is not yet ready or it is fetching data from the homeserver.
- MXKDataSourceStateReady: the data source data is ready.
- MXKDataSourceStateFailed: the data source failed to fetch data.
There is no way in Matrix to be notified when there is a change.
*/
@interface MXKDirectoryServersDataSource : MXKDataSource <UITableViewDataSource>
{
@protected
/**
The data for the cells served by `DirectoryServersDataSource`.
*/
NSMutableArray<id<MXKDirectoryServerCellDataStoring>> *cellDataArray;
/**
The filtered servers: sub-list of `cellDataArray` defined by `searchWithPatterns:`.
*/
NSMutableArray<id<MXKDirectoryServerCellDataStoring>> *filteredCellDataArray;
}
/**
Additional room directory servers the datasource will list.
*/
@property (nonatomic) NSArray<NSString*> *roomDirectoryServers;
/**
Fetch the data source data.
*/
- (void)loadData;
/**
Filter the current recents list according to the provided patterns.
When patterns are not empty, the search result is stored in `filteredCellDataArray`,
this array provides then data for the cells served by `MXKDirectoryServersDataSource`.
@param patternsList the list of patterns to match with. Set nil to cancel search.
*/
- (void)searchWithPatterns:(NSArray<NSString*> *)patternsList;
/**
Get the data for the cell at the given index path.
@param indexPath the index of the cell.
@return the cell data.
*/
- (id<MXKDirectoryServerCellDataStoring>)cellDataAtIndexPath:(NSIndexPath*)indexPath;
@end
@@ -0,0 +1,230 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKDirectoryServersDataSource.h"
#import "MXKDirectoryServerCellData.h"
NSString *const kMXKDirectorServerCellIdentifier = @"kMXKDirectorServerCellIdentifier";
#pragma mark - DirectoryServersDataSource
@interface MXKDirectoryServersDataSource ()
{
// The pending request to load third-party protocols.
MXHTTPOperation *request;
}
@end
@implementation MXKDirectoryServersDataSource
- (instancetype)init
{
self = [super init];
if (self)
{
cellDataArray = [NSMutableArray array];
filteredCellDataArray = nil;
// Set default data w classes
[self registerCellDataClass:MXKDirectoryServerCellData.class forCellIdentifier:kMXKDirectorServerCellIdentifier];
}
return self;
}
- (void)destroy
{
cellDataArray = nil;
filteredCellDataArray = nil;
}
- (void)cancelAllRequests
{
[super cancelAllRequests];
[request cancel];
request = nil;
}
- (void)loadData
{
// Cancel the previous request
if (request)
{
[request cancel];
}
// Reset all vars
[cellDataArray removeAllObjects];
[self setState:MXKDataSourceStatePreparing];
Class class = [self cellDataClassForCellIdentifier:kMXKDirectorServerCellIdentifier];
// Add user's HS
NSString *userHomeserver = self.mxSession.matrixRestClient.credentials.homeServerName;
id<MXKDirectoryServerCellDataStoring> cellData = [[class alloc] initWithHomeserver:userHomeserver includeAllNetworks:YES];
[cellDataArray addObject:cellData];
// Add user's HS but for Matrix public rooms only
cellData = [[class alloc] initWithHomeserver:userHomeserver includeAllNetworks:NO];
[cellDataArray addObject:cellData];
// Add custom directory servers
for (NSString *homeserver in _roomDirectoryServers)
{
if (![homeserver isEqualToString:userHomeserver])
{
cellData = [[class alloc] initWithHomeserver:homeserver includeAllNetworks:YES];
[cellDataArray addObject:cellData];
}
}
MXWeakify(self);
request = [self.mxSession.matrixRestClient thirdpartyProtocols:^(MXThirdpartyProtocolsResponse *thirdpartyProtocolsResponse) {
MXStrongifyAndReturnIfNil(self);
for (NSString *protocolName in thirdpartyProtocolsResponse.protocols)
{
MXThirdPartyProtocol *protocol = thirdpartyProtocolsResponse.protocols[protocolName];
for (MXThirdPartyProtocolInstance *instance in protocol.instances)
{
id<MXKDirectoryServerCellDataStoring> cellData = [[class alloc] initWithProtocolInstance:instance protocol:protocol];
cellData.mediaManager = self.mxSession.mediaManager;
[self->cellDataArray addObject:cellData];
}
}
[self setState:MXKDataSourceStateReady];
} failure:^(NSError *error) {
MXStrongifyAndReturnIfNil(self);
if (!self->request || self->request.isCancelled)
{
// Do not take into account error coming from a cancellation
return;
}
self->request = nil;
MXLogDebug(@"[MXKDirectoryServersDataSource] Failed to fecth third-party protocols. The HS may be too old to support third party networks");
[self setState:MXKDataSourceStateReady];
}];
}
- (void)searchWithPatterns:(NSArray*)patternsList
{
if (patternsList.count)
{
if (filteredCellDataArray)
{
[filteredCellDataArray removeAllObjects];
}
else
{
filteredCellDataArray = [NSMutableArray arrayWithCapacity:cellDataArray.count];
}
for (id<MXKDirectoryServerCellDataStoring> cellData in cellDataArray)
{
for (NSString* pattern in patternsList)
{
if ([cellData.desc rangeOfString:pattern options:NSCaseInsensitiveSearch].location != NSNotFound)
{
[filteredCellDataArray addObject:cellData];
break;
}
}
}
}
else
{
filteredCellDataArray = nil;
}
if (self.delegate)
{
[self.delegate dataSource:self didCellChange:nil];
}
}
/**
Get the data for the cell at the given index path.
@param indexPath the index of the cell.
@return the cell data.
*/
- (id<MXKDirectoryServerCellDataStoring>)cellDataAtIndexPath:(NSIndexPath*)indexPath;
{
if (filteredCellDataArray)
{
return filteredCellDataArray[indexPath.row];
}
return cellDataArray[indexPath.row];
}
#pragma mark - Private methods
// Update the MXKDataSource state and the delegate
- (void)setState:(MXKDataSourceState)newState
{
state = newState;
if (self.delegate && [self.delegate respondsToSelector:@selector(dataSource:didStateChange:)])
{
[self.delegate dataSource:self didStateChange:state];
}
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (filteredCellDataArray)
{
return filteredCellDataArray.count;
}
return cellDataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
id<MXKDirectoryServerCellDataStoring> cellData = [self cellDataAtIndexPath:indexPath];
if (cellData && self.delegate)
{
NSString *identifier = [self.delegate cellReuseIdentifierForCellData:cellData];
if (identifier)
{
UITableViewCell<MXKCellRendering> *cell = [tableView dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath];
// Make the cell display the data
[cell render:cellData];
return cell;
}
}
// Return a fake cell to prevent app from crashing.
return [[UITableViewCell alloc] init];
}
@end
@@ -0,0 +1,212 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <MatrixSDK/MatrixSDK.h>
@class MXKUTI;
NS_ASSUME_NONNULL_BEGIN
extern NSString * const kMXKAttachmentErrorDomain;
/**
List attachment types
*/
typedef enum : NSUInteger {
MXKAttachmentTypeUndefined,
MXKAttachmentTypeImage,
MXKAttachmentTypeAudio,
MXKAttachmentTypeVoiceMessage,
MXKAttachmentTypeVideo,
MXKAttachmentTypeLocation,
MXKAttachmentTypeFile,
MXKAttachmentTypeSticker
} MXKAttachmentType;
/**
`MXKAttachment` represents a room attachment.
*/
@interface MXKAttachment : NSObject
/**
The media manager instance used to download the attachment data.
*/
@property (nonatomic, readonly) MXMediaManager *mediaManager;
/**
The attachment type.
*/
@property (nonatomic, readonly) MXKAttachmentType type;
/**
The attachment information retrieved from the event content during the initialisation.
*/
@property (nonatomic, readonly, nullable) NSString *eventId;
@property (nonatomic, readonly, nullable) NSString *eventRoomId;
@property (nonatomic, readonly) MXEventSentState eventSentState;
@property (nonatomic, readonly, nullable) NSString *contentURL;
@property (nonatomic, readonly, nullable) NSDictionary *contentInfo;
/**
The URL of a 'standard size' thumbnail.
*/
@property (nonatomic, readonly, nullable) NSString *mxcThumbnailURI;
@property (nonatomic, readonly, nullable) NSString *thumbnailMimeType;
/**
The download identifier of the attachment content (related to contentURL).
*/
@property (nonatomic, readonly, nullable) NSString *downloadId;
/**
The download identifier of the attachment thumbnail.
*/
@property (nonatomic, readonly, nullable) NSString *thumbnailDownloadId;
/**
The attached video thumbnail information.
*/
@property (nonatomic, readonly, nullable) NSDictionary *thumbnailInfo;
/**
The original file name retrieved from the event body (if any).
*/
@property (nonatomic, readonly, nullable) NSString *originalFileName;
/**
The thumbnail orientation (relevant in case of image).
*/
@property (nonatomic, readonly) UIImageOrientation thumbnailOrientation;
/**
The cache file path of the attachment.
*/
@property (nonatomic, readonly, nullable) NSString *cacheFilePath;
/**
The cache file path of the attachment thumbnail (may be nil).
*/
@property (nonatomic, readonly, nullable) NSString *thumbnailCachePath;
/**
The preview of the attachment (nil by default).
*/
@property (nonatomic, nullable) UIImage *previewImage;
/**
True if the attachment is encrypted
The encryption status of the thumbnail is not covered by this
property: it is possible for the thumbnail to be encrypted
whether this peoperty is true or false.
*/
@property (nonatomic, readonly) BOOL isEncrypted;
/**
The UTI of this attachment.
*/
@property (nonatomic, readonly, nullable) MXKUTI *uti;
/**
Create a `MXKAttachment` instance for the passed event.
The created instance copies the current data of the event (content, event id, sent state...).
It will ignore any future changes of these data.
@param event a matrix event.
@param mediaManager the media manager instance used to download the attachment data.
@return `MXKAttachment` instance.
*/
- (nullable instancetype)initWithEvent:(MXEvent*)event andMediaManager:(MXMediaManager*)mediaManager;
- (void)destroy;
/**
Gets the thumbnail for this attachment if it is in the memory or disk cache,
otherwise return nil
*/
- (nullable UIImage *)getCachedThumbnail;
/**
For image attachments, gets a UIImage for the full-res image
*/
- (void)getImage:(void (^_Nullable)(MXKAttachment *_Nullable, UIImage *_Nullable))onSuccess failure:(void (^_Nullable)(MXKAttachment *_Nullable, NSError * _Nullable error))onFailure;
/**
Decrypt the attachment data into memory and provide it as an NSData
*/
- (void)getAttachmentData:(void (^_Nullable)(NSData *_Nullable))onSuccess failure:(void (^_Nullable)(NSError * _Nullable error))onFailure;
/**
Decrypts the attachment to a newly created temporary file.
If the isEncrypted property is YES, this method (or getImage) should be used to
obtain the full decrypted attachment. The behaviour of this method is undefined
if isEncrypted is NO.
It is the caller's responsibility to delete the temporary file once it is no longer
needed.
*/
- (void)decryptToTempFile:(void (^_Nullable)(NSString *_Nullable))onSuccess failure:(void (^_Nullable)(NSError * _Nullable error))onFailure;
/** Deletes all previously created temporary files */
+ (void)clearCache;
/**
Gets the thumbnails for this attachment, downloading it or loading it from disk cache
if necessary
*/
- (void)getThumbnail:(void (^_Nullable)(MXKAttachment *_Nullable, UIImage *_Nullable))onSuccess failure:(void (^_Nullable)(MXKAttachment *_Nullable, NSError * _Nullable error))onFailure;
/**
Download the attachment data if it is not already cached.
@param onAttachmentReady block called when attachment is available at 'cacheFilePath'.
@param onFailure the block called on failure.
*/
- (void)prepare:(void (^_Nullable)(void))onAttachmentReady failure:(void (^_Nullable)(NSError * _Nullable error))onFailure;
/**
Save the attachment in user's photo library. This operation is available only for images and video.
@param onSuccess the block called on success.
@param onFailure the block called on failure.
*/
- (void)save:(void (^_Nullable)(void))onSuccess failure:(void (^_Nullable)(NSError * _Nullable error))onFailure;
/**
Copy the attachment data in general pasteboard.
@param onSuccess the block called on success.
@param onFailure the block called on failure.
*/
- (void)copy:(void (^_Nullable)(void))onSuccess failure:(void (^_Nullable)(NSError * _Nullable error))onFailure;
/**
Prepare the attachment data to share it. The original name of the attachment (if any) is used
to name the prepared file.
The developer must call 'onShareEnd' when share operation is ended in order to release potential
resources allocated here.
@param onReadyToShare the block called when attachment is ready to share at the provided file URL.
@param onFailure the block called on failure.
*/
- (void)prepareShare:(void (^_Nullable)(NSURL * _Nullable fileURL))onReadyToShare failure:(void (^_Nullable)(NSError * _Nullable error))onFailure;
- (void)onShareEnded;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,718 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKAttachment.h"
#import "MXKSwiftHeader.h"
@import MatrixSDK;
@import MobileCoreServices;
#import "MXKTools.h"
// The size of thumbnail we request from the server
// Note that this is smaller than the ones we upload: when sending, one size
// must fit all, including the web which will want relatively high res thumbnails.
// We, however, are a mobile client and so would prefer smaller thumbnails, which
// we can have if they're being generated by the media repo.
static const int kThumbnailWidth = 320;
static const int kThumbnailHeight = 240;
NSString *const kMXKAttachmentErrorDomain = @"kMXKAttachmentErrorDomain";
NSString *const kMXKAttachmentFileNameBase = @"attatchment";
@interface MXKAttachment ()
{
/**
The information on the encrypted content.
*/
MXEncryptedContentFile *contentFile;
/**
The information on the encrypted thumbnail.
*/
MXEncryptedContentFile *thumbnailFile;
/**
Observe Attachment download
*/
id onAttachmentDownloadObs;
/**
The local path used to store the attachment with its original name
*/
NSString *documentCopyPath;
/**
The attachment mimetype.
*/
NSString *mimetype;
}
@end
@implementation MXKAttachment
- (instancetype)initWithEvent:(MXEvent*)event andMediaManager:(MXMediaManager*)mediaManager
{
self = [super init];
if (self)
{
_mediaManager = mediaManager;
// Make a copy as the data can be read at anytime later
_eventId = event.eventId;
_eventRoomId = event.roomId;
_eventSentState = event.sentState;
NSDictionary *eventContent = event.content;
// Set default thumbnail orientation
_thumbnailOrientation = UIImageOrientationUp;
if (event.eventType == MXEventTypeSticker)
{
_type = MXKAttachmentTypeSticker;
MXJSONModelSetDictionary(_thumbnailInfo, eventContent[@"info"][@"thumbnail_info"]);
}
else
{
// Note: mxEvent.eventType is supposed to be MXEventTypeRoomMessage here.
NSString *msgtype = eventContent[@"msgtype"];
if ([msgtype isEqualToString:kMXMessageTypeImage])
{
_type = MXKAttachmentTypeImage;
}
else if (event.isVoiceMessage)
{
_type = MXKAttachmentTypeVoiceMessage;
}
else if ([msgtype isEqualToString:kMXMessageTypeAudio])
{
_type = MXKAttachmentTypeAudio;
}
else if ([msgtype isEqualToString:kMXMessageTypeVideo])
{
_type = MXKAttachmentTypeVideo;
MXJSONModelSetDictionary(_thumbnailInfo, eventContent[@"info"][@"thumbnail_info"]);
}
else if ([msgtype isEqualToString:kMXMessageTypeLocation])
{
// Not supported yet
// _type = MXKAttachmentTypeLocation;
return nil;
}
else if ([msgtype isEqualToString:kMXMessageTypeFile])
{
_type = MXKAttachmentTypeFile;
}
else
{
return nil;
}
}
MXJSONModelSetString(_originalFileName, eventContent[@"body"]);
MXJSONModelSetDictionary(_contentInfo, eventContent[@"info"]);
MXJSONModelSetMXJSONModel(contentFile, MXEncryptedContentFile, eventContent[@"file"]);
// Retrieve the content url by taking into account the potential encryption.
if (contentFile)
{
_isEncrypted = YES;
_contentURL = contentFile.url;
MXJSONModelSetMXJSONModel(thumbnailFile, MXEncryptedContentFile, _contentInfo[@"thumbnail_file"]);
}
else
{
_isEncrypted = NO;
MXJSONModelSetString(_contentURL, eventContent[@"url"]);
}
mimetype = nil;
if (_contentInfo)
{
MXJSONModelSetString(mimetype, _contentInfo[@"mimetype"]);
}
_cacheFilePath = [MXMediaManager cachePathForMatrixContentURI:_contentURL andType:mimetype inFolder:_eventRoomId];
_downloadId = [MXMediaManager downloadIdForMatrixContentURI:_contentURL inFolder:_eventRoomId];
// Deduce the thumbnail information from the retrieved data.
_mxcThumbnailURI = [self getThumbnailURI];
_thumbnailMimeType = [self getThumbnailMimeType];
_thumbnailCachePath = [self getThumbnailCachePath];
_thumbnailDownloadId = [self getThumbnailDownloadId];
}
return self;
}
- (void)dealloc
{
[self destroy];
}
- (void)destroy
{
if (onAttachmentDownloadObs)
{
[[NSNotificationCenter defaultCenter] removeObserver:onAttachmentDownloadObs];
onAttachmentDownloadObs = nil;
}
// Remove the temporary file created to prepare attachment sharing
if (documentCopyPath)
{
[[NSFileManager defaultManager] removeItemAtPath:documentCopyPath error:nil];
documentCopyPath = nil;
}
_previewImage = nil;
}
- (NSString *)getThumbnailURI
{
if (thumbnailFile)
{
// there's an encrypted thumbnail: we return the mxc url
return thumbnailFile.url;
}
// Look for a clear thumbnail url
return _contentInfo[@"thumbnail_url"];
}
- (NSString *)getThumbnailMimeType
{
return _thumbnailInfo[@"mimetype"];
}
- (NSString*)getThumbnailCachePath
{
if (_mxcThumbnailURI)
{
return [MXMediaManager cachePathForMatrixContentURI:_mxcThumbnailURI andType:_thumbnailMimeType inFolder:_eventRoomId];
}
// In case of an unencrypted image, consider the thumbnail URI deduced from the content URL, except if
// the attachment is currently uploading.
// Note: When the uploading is in progress, the upload id is stored in the content url (nasty trick).
else if (_type == MXKAttachmentTypeImage && !_isEncrypted && _contentURL && ![_contentURL hasPrefix:kMXMediaUploadIdPrefix])
{
return [MXMediaManager thumbnailCachePathForMatrixContentURI:_contentURL
andType:@"image/jpeg"
inFolder:_eventRoomId
toFitViewSize:CGSizeMake(kThumbnailWidth, kThumbnailHeight)
withMethod:MXThumbnailingMethodScale];
}
return nil;
}
- (NSString *)getThumbnailDownloadId
{
if (_mxcThumbnailURI)
{
return [MXMediaManager downloadIdForMatrixContentURI:_mxcThumbnailURI inFolder:_eventRoomId];
}
// In case of an unencrypted image, consider the thumbnail URI deduced from the content URL, except if
// the attachment is currently uploading.
// Note: When the uploading is in progress, the upload id is stored in the content url (nasty trick).
else if (_type == MXKAttachmentTypeImage && !_isEncrypted && _contentURL && ![_contentURL hasPrefix:kMXMediaUploadIdPrefix])
{
return [MXMediaManager thumbnailDownloadIdForMatrixContentURI:_contentURL
inFolder:_eventRoomId
toFitViewSize:CGSizeMake(kThumbnailWidth, kThumbnailHeight)
withMethod:MXThumbnailingMethodScale];
}
return nil;
}
- (UIImage *)getCachedThumbnail
{
if (_thumbnailCachePath)
{
UIImage *thumb = [MXMediaManager getFromMemoryCacheWithFilePath:_thumbnailCachePath];
if (thumb) return thumb;
if ([[NSFileManager defaultManager] fileExistsAtPath:_thumbnailCachePath])
{
return [MXMediaManager loadThroughCacheWithFilePath:_thumbnailCachePath];
}
}
return nil;
}
- (void)getThumbnail:(void (^)(MXKAttachment *, UIImage *))onSuccess failure:(void (^)(MXKAttachment *, NSError *error))onFailure
{
// Check whether a thumbnail is defined.
if (!_thumbnailCachePath)
{
// there is no thumbnail: if we're an image, return the full size image. Otherwise, nothing we can do.
if (_type == MXKAttachmentTypeImage)
{
[self getImage:onSuccess failure:onFailure];
}
else if (onFailure)
{
onFailure(self, nil);
}
return;
}
// Check the current memory cache.
UIImage *thumb = [MXMediaManager getFromMemoryCacheWithFilePath:_thumbnailCachePath];
if (thumb)
{
onSuccess(self, thumb);
return;
}
if (thumbnailFile)
{
MXWeakify(self);
void (^decryptAndCache)(void) = ^{
MXStrongifyAndReturnIfNil(self);
NSInputStream *instream = [[NSInputStream alloc] initWithFileAtPath:self.thumbnailCachePath];
NSOutputStream *outstream = [[NSOutputStream alloc] initToMemory];
[MXEncryptedAttachments decryptAttachment:self->thumbnailFile inputStream:instream outputStream:outstream success:^{
UIImage *img = [UIImage imageWithData:[outstream propertyForKey:NSStreamDataWrittenToMemoryStreamKey]];
// Save this image to in-memory cache.
[MXMediaManager cacheImage:img withCachePath:self.thumbnailCachePath];
onSuccess(self, img);
} failure:^(NSError *err) {
if (err) {
MXLogDebug(@"Error decrypting attachment! %@", err.userInfo);
if (onFailure) onFailure(self, err);
return;
}
}];
};
if ([[NSFileManager defaultManager] fileExistsAtPath:_thumbnailCachePath])
{
decryptAndCache();
}
else
{
[_mediaManager downloadEncryptedMediaFromMatrixContentFile:thumbnailFile
mimeType:_thumbnailMimeType
inFolder:_eventRoomId
success:^(NSString *outputFilePath) {
decryptAndCache();
}
failure:^(NSError *error) {
if (onFailure) onFailure(self, error);
}];
}
}
else
{
if ([[NSFileManager defaultManager] fileExistsAtPath:_thumbnailCachePath])
{
onSuccess(self, [MXMediaManager loadThroughCacheWithFilePath:_thumbnailCachePath]);
}
else if (_mxcThumbnailURI)
{
[_mediaManager downloadMediaFromMatrixContentURI:_mxcThumbnailURI
withType:_thumbnailMimeType
inFolder:_eventRoomId
success:^(NSString *outputFilePath) {
// Here outputFilePath = thumbnailCachePath
onSuccess(self, [MXMediaManager loadThroughCacheWithFilePath:outputFilePath]);
}
failure:^(NSError *error) {
if (onFailure) onFailure(self, error);
}];
}
else
{
// Here _thumbnailCachePath is defined, so a thumbnail is available.
// Because _mxcThumbnailURI is null, this means we have to consider the content uri (see getThumbnailCachePath).
[_mediaManager downloadThumbnailFromMatrixContentURI:_contentURL
withType:@"image/jpeg"
inFolder:_eventRoomId
toFitViewSize:CGSizeMake(kThumbnailWidth, kThumbnailHeight)
withMethod:MXThumbnailingMethodScale
success:^(NSString *outputFilePath) {
// Here outputFilePath = thumbnailCachePath
onSuccess(self, [MXMediaManager loadThroughCacheWithFilePath:outputFilePath]);
}
failure:^(NSError *error) {
if (onFailure) onFailure(self, error);
}];
}
}
}
- (void)getImage:(void (^)(MXKAttachment *, UIImage *))onSuccess failure:(void (^)(MXKAttachment *, NSError *error))onFailure
{
[self getAttachmentData:^(NSData *data) {
UIImage *img = [UIImage imageWithData:data];
if (img)
{
if (onSuccess)
{
onSuccess(self, img);
}
}
else
{
if (onFailure)
{
NSError *error = [NSError errorWithDomain:kMXKAttachmentErrorDomain code:0 userInfo:@{@"err": @"error_get_image_from_data"}];
onFailure(self, error);
}
}
} failure:^(NSError *error) {
if (onFailure) onFailure(self, error);
}];
}
- (void)getAttachmentData:(void (^)(NSData *))onSuccess failure:(void (^)(NSError *error))onFailure
{
MXWeakify(self);
[self prepare:^{
MXStrongifyAndReturnIfNil(self);
if (self.isEncrypted)
{
// decrypt the encrypted file
NSInputStream *instream = [[NSInputStream alloc] initWithFileAtPath:self.cacheFilePath];
NSOutputStream *outstream = [[NSOutputStream alloc] initToMemory];
[MXEncryptedAttachments decryptAttachment:self->contentFile inputStream:instream outputStream:outstream success:^{
onSuccess([outstream propertyForKey:NSStreamDataWrittenToMemoryStreamKey]);
} failure:^(NSError *err) {
if (err)
{
MXLogDebug(@"Error decrypting attachment! %@", err.userInfo);
return;
}
}];
}
else
{
onSuccess([NSData dataWithContentsOfFile:self.cacheFilePath]);
}
} failure:onFailure];
}
- (void)decryptToTempFile:(void (^)(NSString *))onSuccess failure:(void (^)(NSError *error))onFailure
{
MXWeakify(self);
[self prepare:^{
MXStrongifyAndReturnIfNil(self);
NSString *tempPath = [self getTempFile];
if (!tempPath)
{
if (onFailure) onFailure([NSError errorWithDomain:kMXKAttachmentErrorDomain code:0 userInfo:@{@"err": @"error_creating_temp_file"}]);
return;
}
NSInputStream *inStream = [NSInputStream inputStreamWithFileAtPath:self.cacheFilePath];
NSOutputStream *outStream = [NSOutputStream outputStreamToFileAtPath:tempPath append:NO];
[MXEncryptedAttachments decryptAttachment:self->contentFile inputStream:inStream outputStream:outStream success:^{
onSuccess(tempPath);
} failure:^(NSError *err) {
if (err) {
if (onFailure) onFailure(err);
return;
}
}];
} failure:onFailure];
}
- (NSString *)getTempFile
{
// create a file with an appropriate extension because iOS detects based on file extension
// all over the place
NSString *ext = [MXTools fileExtensionFromContentType:mimetype];
NSString *filenameTemplate = [NSString stringWithFormat:@"%@.XXXXXX%@", kMXKAttachmentFileNameBase, ext];
NSString *template = [NSTemporaryDirectory() stringByAppendingPathComponent:filenameTemplate];
const char *templateCstr = [template fileSystemRepresentation];
char *tempPathCstr = (char *)malloc(strlen(templateCstr) + 1);
strcpy(tempPathCstr, templateCstr);
int fd = mkstemps(tempPathCstr, (int)ext.length);
if (!fd)
{
return nil;
}
close(fd);
NSString *tempPath = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:tempPathCstr
length:strlen(tempPathCstr)];
free(tempPathCstr);
return tempPath;
}
+ (void)clearCache
{
NSString *temporaryDirectoryPath = NSTemporaryDirectory();
NSDirectoryEnumerator<NSString *> *enumerator = [NSFileManager.defaultManager enumeratorAtPath:temporaryDirectoryPath];
NSString *filePath;
while (filePath = [enumerator nextObject]) {
if(![filePath containsString:kMXKAttachmentFileNameBase]) {
continue;
}
NSError *error;
BOOL result = [NSFileManager.defaultManager removeItemAtPath:[temporaryDirectoryPath stringByAppendingPathComponent:filePath] error:&error];
if (!result && error) {
MXLogError(@"[MXKAttachment] Failed deleting temporary file with error: %@", error);
}
}
}
- (void)prepare:(void (^)(void))onAttachmentReady failure:(void (^)(NSError *error))onFailure
{
if ([[NSFileManager defaultManager] fileExistsAtPath:_cacheFilePath])
{
// Done
if (onAttachmentReady)
{
onAttachmentReady();
}
}
else
{
// Trigger download if it is not already in progress
MXMediaLoader* loader = [MXMediaManager existingDownloaderWithIdentifier:_downloadId];
if (!loader)
{
if (_isEncrypted)
{
loader = [_mediaManager downloadEncryptedMediaFromMatrixContentFile:contentFile
mimeType:mimetype
inFolder:_eventRoomId];
}
else
{
loader = [_mediaManager downloadMediaFromMatrixContentURI:_contentURL
withType:mimetype
inFolder:_eventRoomId];
}
}
if (loader)
{
MXWeakify(self);
// Add observers
onAttachmentDownloadObs = [[NSNotificationCenter defaultCenter] addObserverForName:kMXMediaLoaderStateDidChangeNotification object:loader queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
MXStrongifyAndReturnIfNil(self);
MXMediaLoader *loader = (MXMediaLoader*)notif.object;
switch (loader.state) {
case MXMediaLoaderStateDownloadCompleted:
[[NSNotificationCenter defaultCenter] removeObserver:self->onAttachmentDownloadObs];
self->onAttachmentDownloadObs = nil;
if (onAttachmentReady)
{
onAttachmentReady ();
}
break;
case MXMediaLoaderStateDownloadFailed:
case MXMediaLoaderStateCancelled:
[[NSNotificationCenter defaultCenter] removeObserver:self->onAttachmentDownloadObs];
self->onAttachmentDownloadObs = nil;
if (onFailure)
{
onFailure (loader.error);
}
break;
default:
break;
}
}];
}
else if (onFailure)
{
onFailure (nil);
}
}
}
- (void)save:(void (^)(void))onSuccess failure:(void (^)(NSError *error))onFailure
{
if (_type == MXKAttachmentTypeImage || _type == MXKAttachmentTypeVideo)
{
MXWeakify(self);
if (self.isEncrypted) {
[self decryptToTempFile:^(NSString *path) {
MXStrongifyAndReturnIfNil(self);
NSURL* url = [NSURL fileURLWithPath:path];
[MXMediaManager saveMediaToPhotosLibrary:url
isImage:(self.type == MXKAttachmentTypeImage)
success:^(NSURL *assetURL){
if (onSuccess)
{
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
onSuccess();
}
}
failure:onFailure];
} failure:onFailure];
}
else
{
[self prepare:^{
MXStrongifyAndReturnIfNil(self);
NSURL* url = [NSURL fileURLWithPath:self.cacheFilePath];
[MXMediaManager saveMediaToPhotosLibrary:url
isImage:(self.type == MXKAttachmentTypeImage)
success:^(NSURL *assetURL){
if (onSuccess)
{
onSuccess();
}
}
failure:onFailure];
} failure:onFailure];
}
}
else
{
// Not supported
if (onFailure)
{
onFailure(nil);
}
}
}
- (void)copy:(void (^)(void))onSuccess failure:(void (^)(NSError *error))onFailure
{
MXWeakify(self);
[self prepare:^{
MXStrongifyAndReturnIfNil(self);
if (self.type == MXKAttachmentTypeImage)
{
[self getImage:^(MXKAttachment *attachment, UIImage *img) {
MXKPasteboardManager.shared.pasteboard.image = img;
if (onSuccess)
{
onSuccess();
}
} failure:^(MXKAttachment *attachment, NSError *error) {
if (onFailure) onFailure(error);
}];
}
else
{
MXWeakify(self);
[self getAttachmentData:^(NSData *data) {
if (data)
{
MXStrongifyAndReturnIfNil(self);
NSString* UTI = (__bridge_transfer NSString *) UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[self.cacheFilePath pathExtension] , NULL);
if (UTI)
{
[MXKPasteboardManager.shared.pasteboard setData:data forPasteboardType:UTI];
if (onSuccess)
{
onSuccess();
}
}
}
} failure:onFailure];
}
// Unexpected error
if (onFailure)
{
onFailure(nil);
}
} failure:onFailure];
}
- (MXKUTI *)uti
{
return [[MXKUTI alloc] initWithMimeType:mimetype];
}
- (void)prepareShare:(void (^)(NSURL *fileURL))onReadyToShare failure:(void (^)(NSError *error))onFailure
{
MXWeakify(self);
void (^haveFile)(NSString *) = ^(NSString *path) {
// Prepare the file URL by considering the original file name (if any)
NSURL *fileUrl;
MXStrongifyAndReturnIfNil(self);
// Check whether the original name retrieved from event body has extension
if (self.originalFileName && [self.originalFileName pathExtension].length)
{
// Copy the cached file to restore its original name
// Note: We used previously symbolic link (instead of copy) but UIDocumentInteractionController failed to open Office documents (.docx, .pptx...).
self->documentCopyPath = [[MXMediaManager getCachePath] stringByAppendingPathComponent:self.originalFileName];
[[NSFileManager defaultManager] removeItemAtPath:self->documentCopyPath error:nil];
if ([[NSFileManager defaultManager] copyItemAtPath:path toPath:self->documentCopyPath error:nil])
{
fileUrl = [NSURL fileURLWithPath:self->documentCopyPath];
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
}
}
if (!fileUrl)
{
// Use the cached file by default
fileUrl = [NSURL fileURLWithPath:path];
self->documentCopyPath = path;
}
onReadyToShare (fileUrl);
};
if (self.isEncrypted)
{
[self decryptToTempFile:^(NSString *path) {
haveFile(path);
} failure:onFailure];
}
else
{
// First download data if it is not already done
[self prepare:^{
haveFile(self.cacheFilePath);
} failure:onFailure];
}
}
- (void)onShareEnded
{
// Remove the temporary file created to prepare attachment sharing
if (documentCopyPath)
{
[[NSFileManager defaultManager] removeItemAtPath:documentCopyPath error:nil];
documentCopyPath = nil;
}
}
@end
@@ -0,0 +1,52 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <MatrixSDK/MatrixSDK.h>
/**
`MXKQueuedEvent` represents an event waiting to be processed.
*/
@interface MXKQueuedEvent : NSObject
/**
The event.
*/
@property (nonatomic, readonly) MXEvent *event;
/**
The state of the room when the event has been received.
*/
@property (nonatomic, readonly) MXRoomState *state;
/**
The direction of reception. Is it a live event or an event from the history?
*/
@property (nonatomic, readonly) MXTimelineDirection direction;
/**
Tells whether the event is queued during server sync or not.
*/
@property (nonatomic) BOOL serverSyncEvent;
/**
Date of the `event`. If event has a valid `originServerTs`, it's converted to a date object, otherwise current date.
*/
@property (nonatomic, readonly) NSDate *eventDate;
- (instancetype)initWithEvent:(MXEvent*)event andRoomState:(MXRoomState*)state direction:(MXTimelineDirection)direction;
@end
@@ -0,0 +1,43 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKQueuedEvent.h"
@implementation MXKQueuedEvent
- (instancetype)initWithEvent:(MXEvent *)event andRoomState:(MXRoomState *)state direction:(MXTimelineDirection)direction
{
self = [super init];
if (self)
{
_event = event;
_state = state;
_direction = direction;
}
return self;
}
- (NSDate *)eventDate
{
if (_event.originServerTs != kMXUndefinedTimestamp)
{
return [NSDate dateWithTimeIntervalSince1970:(double)_event.originServerTs/1000];
}
return [NSDate date];
}
@end
@@ -0,0 +1,166 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKCellData.h"
#import "MXKRoomBubbleCellDataStoring.h"
#import "MXKRoomBubbleComponent.h"
#define MXKROOMBUBBLECELLDATA_TEXTVIEW_DEFAULT_VERTICAL_INSET 8
/**
`MXKRoomBubbleCellData` instances compose data for `MXKRoomBubbleTableViewCell` cells.
This is the basic implementation which considers only one component (event) by bubble.
`MXKRoomBubbleCellDataWithAppendingMode` extends this class to merge consecutive messages from the same sender into one bubble.
*/
@interface MXKRoomBubbleCellData : MXKCellData <MXKRoomBubbleCellDataStoring>
{
@protected
/**
The data source owner of this instance.
*/
__weak MXKRoomDataSource *roomDataSource;
/**
Array of bubble components. Each bubble is supposed to have at least one component.
*/
NSMutableArray *bubbleComponents;
/**
The body of the message with sets of attributes, or kind of content description in case of attachment (e.g. "image attachment")
*/
NSAttributedString *attributedTextMessage;
/**
The optional text pattern to be highlighted in the body of the message.
*/
NSString *highlightedPattern;
UIColor *highlightedPatternColor;
UIFont *highlightedPatternFont;
}
/**
The matrix session.
*/
@property (nonatomic, readonly) MXSession *mxSession;
/**
Returns bubble components list (`MXKRoomBubbleComponent` instances).
*/
@property (nonatomic, readonly) NSArray<MXKRoomBubbleComponent*> *bubbleComponents;
/**
Read receipts per event.
*/
@property(nonatomic) NSMutableDictionary<NSString* /* eventId */,
NSArray<MXReceiptData *> *> *readReceipts;
/**
Aggregated reactions per event.
*/
@property(nonatomic) NSMutableDictionary<NSString* /* eventId */, MXAggregatedReactions*> *reactions;
/**
Whether there is a link to preview in the components.
*/
@property (nonatomic, readonly) BOOL hasLink;
/**
Event formatter
*/
@property (nonatomic) MXKEventFormatter *eventFormatter;
/**
The max width of the text view used to display the text message (relevant only for text message or attached file).
*/
@property (nonatomic) CGFloat maxTextViewWidth;
/**
The bubble content size depends on its type:
- Text: returns suitable content size of a text view to display the whole text message (respecting maxTextViewWidth).
- Attached image or video: returns suitable content size for an image view in order to display
attachment thumbnail or icon.
- Attached file: returns suitable content size of a text view to display the file name (no icon is used presently).
*/
@property (nonatomic) CGSize contentSize;
/**
Set of flags indicating fixes that need to be applied at display time.
*/
@property (nonatomic, readonly) MXKRoomBubbleComponentDisplayFix displayFix;
/**
Attachment upload
*/
@property (nonatomic) NSString *uploadId;
@property (nonatomic) CGFloat uploadProgress;
/**
Indicate a bubble component needs to show encryption badge.
*/
@property (nonatomic, readonly) BOOL containsBubbleComponentWithEncryptionBadge;
/**
Indicate that the current text message layout is no longer valid and should be recomputed
before presentation in a bubble cell. This could be due to the content changing, or the
available space for the cell has been updated.
This will clear the current `attributedTextMessage` allowing it to be
rebuilt on demand when requested.
*/
- (void)invalidateTextLayout;
/**
Check and refresh the position of each component.
*/
- (void)prepareBubbleComponentsPosition;
/**
Return the raw height of the provided text by removing any vertical margin/inset.
@param attributedText the attributed text to measure
@return the computed height
*/
- (CGFloat)rawTextHeight:(NSAttributedString*)attributedText;
/**
Return the content size of a text view initialized with the provided attributed text.
CAUTION: This method runs only on main thread.
@param attributedText the attributed text to measure
@param removeVerticalInset tell whether the computation should remove vertical inset in text container.
@return the computed size content
*/
- (CGSize)textContentSize:(NSAttributedString*)attributedText removeVerticalInset:(BOOL)removeVerticalInset;
/**
Get bubble component index from event id.
@param eventId Event id of bubble component.
@return Index of bubble component associated to event id or NSNotFound
*/
- (NSInteger)bubbleComponentIndexForEventId:(NSString *)eventId;
/**
Get the first visible component.
@return First visible component or nil.
*/
- (MXKRoomBubbleComponent*)getFirstBubbleComponentWithDisplay;
@end
@@ -0,0 +1,923 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#define MXKROOMBUBBLECELLDATA_MAX_ATTACHMENTVIEW_WIDTH 192
#define MXKROOMBUBBLECELLDATA_DEFAULT_MAX_TEXTVIEW_WIDTH 200
@import MatrixSDK;
#import "MXKRoomBubbleCellData.h"
#import "MXKTools.h"
@implementation MXKRoomBubbleCellData
@synthesize senderId, targetId, roomId, senderDisplayName, senderAvatarUrl, senderAvatarPlaceholder, targetDisplayName, targetAvatarUrl, targetAvatarPlaceholder, isEncryptedRoom, isPaginationFirstBubble, shouldHideSenderInformation, date, isIncoming, isAttachmentWithThumbnail, isAttachmentWithIcon, attachment, senderFlair;
@synthesize textMessage, attributedTextMessage;
@synthesize shouldHideSenderName, isTyping, showBubbleDateTime, showBubbleReceipts, useCustomDateTimeLabel, useCustomReceipts, useCustomUnsentButton, hasNoDisplay;
@synthesize tag;
@synthesize collapsable, collapsed, collapsedAttributedTextMessage, prevCollapsableCellData, nextCollapsableCellData, collapseState;
#pragma mark - MXKRoomBubbleCellDataStoring
- (instancetype)initWithEvent:(MXEvent *)event andRoomState:(MXRoomState *)roomState andRoomDataSource:(MXKRoomDataSource *)roomDataSource2
{
self = [self init];
if (self)
{
roomDataSource = roomDataSource2;
// Initialize read receipts
self.readReceipts = [NSMutableDictionary dictionary];
// Create the bubble component based on matrix event
MXKRoomBubbleComponent *firstComponent = [[MXKRoomBubbleComponent alloc] initWithEvent:event roomState:roomState eventFormatter:roomDataSource.eventFormatter session:roomDataSource.mxSession];
if (firstComponent)
{
bubbleComponents = [NSMutableArray array];
[bubbleComponents addObject:firstComponent];
senderId = event.sender;
targetId = [event.type isEqualToString:kMXEventTypeStringRoomMember] ? event.stateKey : nil;
roomId = roomDataSource.roomId;
senderDisplayName = [roomDataSource.eventFormatter senderDisplayNameForEvent:event withRoomState:roomState];
senderAvatarUrl = [roomDataSource.eventFormatter senderAvatarUrlForEvent:event withRoomState:roomState];
senderAvatarPlaceholder = nil;
targetDisplayName = [roomDataSource.eventFormatter targetDisplayNameForEvent:event withRoomState:roomState];
targetAvatarUrl = [roomDataSource.eventFormatter targetAvatarUrlForEvent:event withRoomState:roomState];
targetAvatarPlaceholder = nil;
isEncryptedRoom = roomState.isEncrypted;
isIncoming = ([event.sender isEqualToString:roomDataSource.mxSession.myUser.userId] == NO);
// Check attachment if any
if ([roomDataSource.eventFormatter isSupportedAttachment:event])
{
// Note: event.eventType is equal here to MXEventTypeRoomMessage or MXEventTypeSticker
attachment = [[MXKAttachment alloc] initWithEvent:event andMediaManager:roomDataSource.mxSession.mediaManager];
if (attachment && attachment.type == MXKAttachmentTypeImage)
{
// Check the current thumbnail orientation. Rotate the current content size (if need)
if (attachment.thumbnailOrientation == UIImageOrientationLeft || attachment.thumbnailOrientation == UIImageOrientationRight)
{
_contentSize = CGSizeMake(_contentSize.height, _contentSize.width);
}
}
}
// Report the attributed string (This will initialize _contentSize attribute)
self.attributedTextMessage = firstComponent.attributedTextMessage;
// Initialize rendering attributes
_maxTextViewWidth = MXKROOMBUBBLECELLDATA_DEFAULT_MAX_TEXTVIEW_WIDTH;
}
else
{
// Ignore this event
self = nil;
}
}
return self;
}
- (void)dealloc
{
// Reset any observer on publicised groups by user.
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidUpdatePublicisedGroupsForUsersNotification object:self.mxSession];
roomDataSource = nil;
bubbleComponents = nil;
}
- (NSUInteger)updateEvent:(NSString *)eventId withEvent:(MXEvent *)event
{
NSUInteger count = 0;
@synchronized(bubbleComponents)
{
// Retrieve the component storing the event and update it
for (NSUInteger index = 0; index < bubbleComponents.count; index++)
{
MXKRoomBubbleComponent *roomBubbleComponent = [bubbleComponents objectAtIndex:index];
if ([roomBubbleComponent.event.eventId isEqualToString:eventId])
{
[roomBubbleComponent updateWithEvent:event roomState:roomDataSource.roomState session:self.mxSession];
if (!roomBubbleComponent.textMessage.length)
{
[bubbleComponents removeObjectAtIndex:index];
}
// Indicate that the text message layout should be recomputed.
[self invalidateTextLayout];
// Handle here attachment update.
// For example: the case of update of attachment event happens when an echo is replaced by its true event
// received back by the events stream.
if (attachment)
{
// Check the current content url, to update it with the actual one
// Retrieve content url/info
NSString *eventContentURL = event.content[@"url"];
if (event.content[@"file"][@"url"])
{
eventContentURL = event.content[@"file"][@"url"];
}
if (!eventContentURL.length)
{
// The attachment has been redacted.
attachment = nil;
_contentSize = CGSizeZero;
}
else if (![attachment.eventId isEqualToString:event.eventId] || ![attachment.contentURL isEqualToString:eventContentURL])
{
MXKAttachment *updatedAttachment = [[MXKAttachment alloc] initWithEvent:event andMediaManager:roomDataSource.mxSession.mediaManager];
// Sanity check on attachment type
if (updatedAttachment && attachment.type == updatedAttachment.type)
{
// Re-use the current image as preview to prevent the cell from flashing
updatedAttachment.previewImage = [attachment getCachedThumbnail];
if (!updatedAttachment.previewImage && attachment.type == MXKAttachmentTypeImage)
{
updatedAttachment.previewImage = [MXMediaManager loadPictureFromFilePath:attachment.cacheFilePath];
}
// Clean the cache by removing the useless data
if (![updatedAttachment.cacheFilePath isEqualToString:attachment.cacheFilePath])
{
[[NSFileManager defaultManager] removeItemAtPath:attachment.cacheFilePath error:nil];
}
if (![updatedAttachment.thumbnailCachePath isEqualToString:attachment.thumbnailCachePath])
{
[[NSFileManager defaultManager] removeItemAtPath:attachment.thumbnailCachePath error:nil];
}
// Update the current attachment description
attachment = updatedAttachment;
if (attachment.type == MXKAttachmentTypeImage)
{
// Reset content size
_contentSize = CGSizeZero;
// Check the current thumbnail orientation. Rotate the current content size (if need)
if (attachment.thumbnailOrientation == UIImageOrientationLeft || attachment.thumbnailOrientation == UIImageOrientationRight)
{
_contentSize = CGSizeMake(_contentSize.height, _contentSize.width);
}
}
}
else
{
MXLogDebug(@"[MXKRoomBubbleCellData] updateEvent: Warning: Does not support change of attachment type");
}
}
}
else if ([roomDataSource.eventFormatter isSupportedAttachment:event])
{
// The event is updated to an event with attachement
attachment = [[MXKAttachment alloc] initWithEvent:event andMediaManager:roomDataSource.mxSession.mediaManager];
if (attachment && attachment.type == MXKAttachmentTypeImage)
{
// Check the current thumbnail orientation. Rotate the current content size (if need)
if (attachment.thumbnailOrientation == UIImageOrientationLeft || attachment.thumbnailOrientation == UIImageOrientationRight)
{
_contentSize = CGSizeMake(_contentSize.height, _contentSize.width);
}
}
}
break;
}
}
count = bubbleComponents.count;
}
return count;
}
- (NSUInteger)removeEvent:(NSString *)eventId
{
NSUInteger count = 0;
@synchronized(bubbleComponents)
{
for (MXKRoomBubbleComponent *roomBubbleComponent in bubbleComponents)
{
if ([roomBubbleComponent.event.eventId isEqualToString:eventId])
{
[bubbleComponents removeObject:roomBubbleComponent];
// Indicate that the text message layout should be recomputed.
[self invalidateTextLayout];
break;
}
}
count = bubbleComponents.count;
}
return count;
}
- (NSUInteger)removeEventsFromEvent:(NSString*)eventId removedEvents:(NSArray<MXEvent*>**)removedEvents;
{
NSMutableArray *cuttedEvents = [NSMutableArray array];
@synchronized(bubbleComponents)
{
NSInteger componentIndex = [self bubbleComponentIndexForEventId:eventId];
if (NSNotFound != componentIndex)
{
NSArray *newBubbleComponents = [bubbleComponents subarrayWithRange:NSMakeRange(0, componentIndex)];
for (NSUInteger i = componentIndex; i < bubbleComponents.count; i++)
{
MXKRoomBubbleComponent *roomBubbleComponent = bubbleComponents[i];
[cuttedEvents addObject:roomBubbleComponent.event];
}
bubbleComponents = [NSMutableArray arrayWithArray:newBubbleComponents];
// Indicate that the text message layout should be recomputed.
[self invalidateTextLayout];
}
}
*removedEvents = cuttedEvents;
return bubbleComponents.count;
}
- (BOOL)hasSameSenderAsBubbleCellData:(id<MXKRoomBubbleCellDataStoring>)bubbleCellData
{
// Sanity check: accept only object of MXKRoomBubbleCellData classes or sub-classes
NSParameterAssert([bubbleCellData isKindOfClass:[MXKRoomBubbleCellData class]]);
// NOTE: Same sender means here same id, same display name and same avatar
// Check first user id
if ([senderId isEqualToString:bubbleCellData.senderId] == NO)
{
return NO;
}
// Check sender name
if ((senderDisplayName.length || bubbleCellData.senderDisplayName.length) && ([senderDisplayName isEqualToString:bubbleCellData.senderDisplayName] == NO))
{
return NO;
}
// Check avatar url
if ((senderAvatarUrl.length || bubbleCellData.senderAvatarUrl.length) && ([senderAvatarUrl isEqualToString:bubbleCellData.senderAvatarUrl] == NO))
{
return NO;
}
return YES;
}
- (MXKRoomBubbleComponent*) getFirstBubbleComponent
{
MXKRoomBubbleComponent* first = nil;
@synchronized(bubbleComponents)
{
if (bubbleComponents.count)
{
first = [bubbleComponents firstObject];
}
}
return first;
}
- (MXKRoomBubbleComponent*) getFirstBubbleComponentWithDisplay
{
// Look for the first component which is actually displayed (some event are ignored in room history display).
MXKRoomBubbleComponent* first = nil;
@synchronized(bubbleComponents)
{
for (NSInteger index = 0; index < bubbleComponents.count; index++)
{
MXKRoomBubbleComponent *component = bubbleComponents[index];
if (component.attributedTextMessage)
{
first = component;
break;
}
}
}
return first;
}
- (NSAttributedString*)attributedTextMessageWithHighlightedEvent:(NSString*)eventId tintColor:(UIColor*)tintColor
{
NSAttributedString *customAttributedTextMsg;
// By default only one component is supported, consider here the first component
MXKRoomBubbleComponent *firstComponent = [self getFirstBubbleComponent];
if (firstComponent)
{
customAttributedTextMsg = firstComponent.attributedTextMessage;
// Sanity check
if (customAttributedTextMsg && [firstComponent.event.eventId isEqualToString:eventId])
{
NSMutableAttributedString *customComponentString = [[NSMutableAttributedString alloc] initWithAttributedString:customAttributedTextMsg];
UIColor *color = tintColor ? tintColor : [UIColor lightGrayColor];
[customComponentString addAttribute:NSBackgroundColorAttributeName value:color range:NSMakeRange(0, customComponentString.length)];
customAttributedTextMsg = customComponentString;
}
}
return customAttributedTextMsg;
}
- (void)highlightPatternInTextMessage:(NSString*)pattern withForegroundColor:(UIColor*)patternColor andFont:(UIFont*)patternFont
{
highlightedPattern = pattern;
highlightedPatternColor = patternColor;
highlightedPatternFont = patternFont;
// Indicate that the text message layout should be recomputed.
[self invalidateTextLayout];
}
- (void)setShouldHideSenderInformation:(BOOL)inShouldHideSenderInformation
{
shouldHideSenderInformation = inShouldHideSenderInformation;
if (!shouldHideSenderInformation)
{
// Refresh the flair
[self refreshSenderFlair];
}
}
- (void)refreshSenderFlair
{
// Reset by default any observer on publicised groups by user.
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidUpdatePublicisedGroupsForUsersNotification object:self.mxSession];
// Check first whether the room enabled the flair for some groups
NSArray<NSString *> *roomRelatedGroups = roomDataSource.roomState.relatedGroups;
if (roomRelatedGroups.count && senderId)
{
NSArray<NSString *> *senderPublicisedGroups;
senderPublicisedGroups = [self.mxSession publicisedGroupsForUser:senderId];
if (senderPublicisedGroups.count)
{
// Cross the 2 arrays to keep only the common group ids
NSMutableArray *flair = [NSMutableArray arrayWithCapacity:roomRelatedGroups.count];
for (NSString *groupId in roomRelatedGroups)
{
if ([senderPublicisedGroups indexOfObject:groupId] != NSNotFound)
{
MXGroup *group = [roomDataSource groupWithGroupId:groupId];
[flair addObject:group];
}
}
if (flair.count)
{
self.senderFlair = flair;
}
else
{
self.senderFlair = nil;
}
}
else
{
self.senderFlair = nil;
}
// Observe any change on publicised groups for the message sender
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionUpdatePublicisedGroupsForUsers:) name:kMXSessionDidUpdatePublicisedGroupsForUsersNotification object:self.mxSession];
}
}
#pragma mark -
- (void)invalidateTextLayout
{
self.attributedTextMessage = nil;
}
- (void)prepareBubbleComponentsPosition
{
// Consider here only the first component if any
MXKRoomBubbleComponent *firstComponent = [self getFirstBubbleComponent];
if (firstComponent)
{
CGFloat positionY = (attachment == nil || attachment.type == MXKAttachmentTypeFile || attachment.type == MXKAttachmentTypeAudio || attachment.type == MXKAttachmentTypeVoiceMessage) ? MXKROOMBUBBLECELLDATA_TEXTVIEW_DEFAULT_VERTICAL_INSET : 0;
firstComponent.position = CGPointMake(0, positionY);
}
}
- (NSInteger)bubbleComponentIndexForEventId:(NSString *)eventId
{
return [self.bubbleComponents indexOfObjectPassingTest:^BOOL(MXKRoomBubbleComponent * _Nonnull bubbleComponent, NSUInteger idx, BOOL * _Nonnull stop) {
if ([bubbleComponent.event.eventId isEqualToString:eventId])
{
*stop = YES;
return YES;
}
return NO;
}];
}
#pragma mark - Text measuring
// Return the raw height of the provided text by removing any margin
- (CGFloat)rawTextHeight: (NSAttributedString*)attributedText
{
__block CGSize textSize;
if ([NSThread currentThread] != [NSThread mainThread])
{
dispatch_sync(dispatch_get_main_queue(), ^{
textSize = [self textContentSize:attributedText removeVerticalInset:YES];
});
}
else
{
textSize = [self textContentSize:attributedText removeVerticalInset:YES];
}
return textSize.height;
}
- (CGSize)textContentSize:(NSAttributedString*)attributedText removeVerticalInset:(BOOL)removeVerticalInset
{
static UITextView* measurementTextView = nil;
static UITextView* measurementTextViewWithoutInset = nil;
if (attributedText.length)
{
if (!measurementTextView)
{
measurementTextView = [[UITextView alloc] init];
measurementTextViewWithoutInset = [[UITextView alloc] init];
// Remove the container inset: this operation impacts only the vertical margin.
// Note: consider textContainer.lineFragmentPadding to remove horizontal margin
measurementTextViewWithoutInset.textContainerInset = UIEdgeInsetsZero;
}
// Select the right text view for measurement
UITextView *selectedTextView = (removeVerticalInset ? measurementTextViewWithoutInset : measurementTextView);
selectedTextView.frame = CGRectMake(0, 0, _maxTextViewWidth, MAXFLOAT);
selectedTextView.attributedText = attributedText;
CGSize size = [selectedTextView sizeThatFits:selectedTextView.frame.size];
// Manage the case where a string attribute has a single paragraph with a left indent
// In this case, [UITextView sizeThatFits] ignores the indent and return the width
// of the text only.
// So, add this indent afterwards
NSRange textRange = NSMakeRange(0, attributedText.length);
NSRange longestEffectiveRange;
NSParagraphStyle *paragraphStyle = [attributedText attribute:NSParagraphStyleAttributeName atIndex:0 longestEffectiveRange:&longestEffectiveRange inRange:textRange];
if (NSEqualRanges(textRange, longestEffectiveRange))
{
size.width = size.width + paragraphStyle.headIndent;
}
return size;
}
return CGSizeZero;
}
#pragma mark - Properties
- (MXSession*)mxSession
{
return roomDataSource.mxSession;
}
- (NSArray*)bubbleComponents
{
NSArray* copy;
@synchronized(bubbleComponents)
{
copy = [bubbleComponents copy];
}
return copy;
}
- (NSString*)textMessage
{
return self.attributedTextMessage.string;
}
- (void)setAttributedTextMessage:(NSAttributedString *)inAttributedTextMessage
{
attributedTextMessage = inAttributedTextMessage;
if (attributedTextMessage.length && highlightedPattern)
{
[self highlightPattern];
}
// Reset content size
_contentSize = CGSizeZero;
}
- (NSAttributedString*)attributedTextMessage
{
if (self.hasAttributedTextMessage && !attributedTextMessage.length)
{
// By default only one component is supported, consider here the first component
MXKRoomBubbleComponent *firstComponent = [self getFirstBubbleComponent];
if (firstComponent)
{
attributedTextMessage = firstComponent.attributedTextMessage;
if (attributedTextMessage.length && highlightedPattern)
{
[self highlightPattern];
}
}
}
return attributedTextMessage;
}
- (BOOL)hasAttributedTextMessage
{
// Determine if the event formatter will return at least one string for the events in this cell.
// No string means that the event formatter has been configured so that it did not accept all events
// of the cell.
BOOL hasAttributedTextMessage = NO;
@synchronized(bubbleComponents)
{
for (MXKRoomBubbleComponent *roomBubbleComponent in bubbleComponents)
{
if (roomBubbleComponent.attributedTextMessage)
{
hasAttributedTextMessage = YES;
break;
}
}
}
return hasAttributedTextMessage;
}
- (BOOL)hasLink
{
@synchronized (bubbleComponents) {
for (MXKRoomBubbleComponent *component in bubbleComponents)
{
if (component.link)
{
return YES;
}
}
}
return NO;
}
- (MXKRoomBubbleComponentDisplayFix)displayFix
{
MXKRoomBubbleComponentDisplayFix displayFix = MXKRoomBubbleComponentDisplayFixNone;
@synchronized(bubbleComponents)
{
for (MXKRoomBubbleComponent *component in self.bubbleComponents)
{
displayFix |= component.displayFix;
}
}
return displayFix;
}
- (BOOL)shouldHideSenderName
{
BOOL res = NO;
MXKRoomBubbleComponent *firstDisplayedComponent = [self getFirstBubbleComponentWithDisplay];
NSString *senderDisplayName = self.senderDisplayName;
if (firstDisplayedComponent)
{
res = (firstDisplayedComponent.event.isEmote || (firstDisplayedComponent.event.isState && senderDisplayName && [firstDisplayedComponent.textMessage hasPrefix:senderDisplayName]));
}
return res;
}
- (NSArray*)events
{
NSMutableArray* eventsArray;
@synchronized(bubbleComponents)
{
eventsArray = [NSMutableArray arrayWithCapacity:bubbleComponents.count];
for (MXKRoomBubbleComponent *roomBubbleComponent in bubbleComponents)
{
if (roomBubbleComponent.event)
{
[eventsArray addObject:roomBubbleComponent.event];
}
}
}
return eventsArray;
}
- (NSDate*)date
{
MXKRoomBubbleComponent *firstDisplayedComponent = [self getFirstBubbleComponentWithDisplay];
if (firstDisplayedComponent)
{
return firstDisplayedComponent.date;
}
return nil;
}
- (BOOL)hasNoDisplay
{
BOOL noDisplay = YES;
// Check whether at least one component has a string description.
@synchronized(bubbleComponents)
{
if (self.collapsed)
{
// Collapsed cells have no display except their cell header
noDisplay = !self.collapsedAttributedTextMessage;
}
else
{
for (MXKRoomBubbleComponent *roomBubbleComponent in bubbleComponents)
{
if (roomBubbleComponent.attributedTextMessage)
{
noDisplay = NO;
break;
}
}
}
}
return (noDisplay && !attachment);
}
- (BOOL)isAttachmentWithThumbnail
{
return (attachment && (attachment.type == MXKAttachmentTypeImage || attachment.type == MXKAttachmentTypeVideo || attachment.type == MXKAttachmentTypeSticker));
}
- (BOOL)isAttachmentWithIcon
{
// Not supported yet (TODO for audio, file).
return NO;
}
- (void)setMaxTextViewWidth:(CGFloat)inMaxTextViewWidth
{
// Check change
if (inMaxTextViewWidth != _maxTextViewWidth)
{
_maxTextViewWidth = inMaxTextViewWidth;
// Reset content size
_contentSize = CGSizeZero;
}
}
- (CGSize)contentSize
{
if (CGSizeEqualToSize(_contentSize, CGSizeZero))
{
if (attachment == nil)
{
// Here the bubble is a text message
if ([NSThread currentThread] != [NSThread mainThread])
{
dispatch_sync(dispatch_get_main_queue(), ^{
self->_contentSize = [self textContentSize:self.attributedTextMessage removeVerticalInset:NO];
});
}
else
{
_contentSize = [self textContentSize:self.attributedTextMessage removeVerticalInset:NO];
}
}
else if (self.isAttachmentWithThumbnail)
{
CGFloat width, height;
// Set default content size
width = height = MXKROOMBUBBLECELLDATA_MAX_ATTACHMENTVIEW_WIDTH;
if (attachment.thumbnailInfo || attachment.contentInfo)
{
if (attachment.thumbnailInfo && attachment.thumbnailInfo[@"w"] && attachment.thumbnailInfo[@"h"])
{
width = [attachment.thumbnailInfo[@"w"] integerValue];
height = [attachment.thumbnailInfo[@"h"] integerValue];
}
else if (attachment.contentInfo[@"w"] && attachment.contentInfo[@"h"])
{
width = [attachment.contentInfo[@"w"] integerValue];
height = [attachment.contentInfo[@"h"] integerValue];
}
if (width > MXKROOMBUBBLECELLDATA_MAX_ATTACHMENTVIEW_WIDTH || height > MXKROOMBUBBLECELLDATA_MAX_ATTACHMENTVIEW_WIDTH)
{
if (width > height)
{
height = (height * MXKROOMBUBBLECELLDATA_MAX_ATTACHMENTVIEW_WIDTH) / width;
height = floorf(height / 2) * 2;
width = MXKROOMBUBBLECELLDATA_MAX_ATTACHMENTVIEW_WIDTH;
}
else
{
width = (width * MXKROOMBUBBLECELLDATA_MAX_ATTACHMENTVIEW_WIDTH) / height;
width = floorf(width / 2) * 2;
height = MXKROOMBUBBLECELLDATA_MAX_ATTACHMENTVIEW_WIDTH;
}
}
}
// Check here thumbnail orientation
if (attachment.thumbnailOrientation == UIImageOrientationLeft || attachment.thumbnailOrientation == UIImageOrientationRight)
{
_contentSize = CGSizeMake(height, width);
}
else
{
_contentSize = CGSizeMake(width, height);
}
}
else if (attachment.type == MXKAttachmentTypeFile || attachment.type == MXKAttachmentTypeAudio)
{
// Presently we displayed only the file name for attached file (no icon yet)
// Return suitable content size of a text view to display the file name (available in text message).
if ([NSThread currentThread] != [NSThread mainThread])
{
dispatch_sync(dispatch_get_main_queue(), ^{
self->_contentSize = [self textContentSize:self.attributedTextMessage removeVerticalInset:NO];
});
}
else
{
_contentSize = [self textContentSize:self.attributedTextMessage removeVerticalInset:NO];
}
}
else
{
_contentSize = CGSizeMake(40, 40);
}
}
return _contentSize;
}
- (MXKEventFormatter *)eventFormatter
{
MXKRoomBubbleComponent *firstComponent = [bubbleComponents firstObject];
// Retrieve event formatter from the first component
if (firstComponent)
{
return firstComponent.eventFormatter;
}
return nil;
}
- (BOOL)showAntivirusScanStatus
{
MXKRoomBubbleComponent *firstBubbleComponent = self.bubbleComponents.firstObject;
if (self.attachment == nil || firstBubbleComponent == nil)
{
return NO;
}
MXEventScan *eventScan = firstBubbleComponent.eventScan;
return eventScan != nil && eventScan.antivirusScanStatus != MXAntivirusScanStatusTrusted;
}
- (BOOL)containsBubbleComponentWithEncryptionBadge
{
BOOL containsBubbleComponentWithEncryptionBadge = NO;
@synchronized(bubbleComponents)
{
for (MXKRoomBubbleComponent *component in bubbleComponents)
{
if (component.showEncryptionBadge)
{
containsBubbleComponentWithEncryptionBadge = YES;
break;
}
}
}
return containsBubbleComponentWithEncryptionBadge;
}
#pragma mark - Bubble collapsing
- (BOOL)collapseWith:(id<MXKRoomBubbleCellDataStoring>)cellData
{
// NO by default
return NO;
}
#pragma mark - Internals
- (void)highlightPattern
{
NSMutableAttributedString *customAttributedTextMsg = nil;
NSString *currentTextMessage = self.textMessage;
NSRange range = [currentTextMessage rangeOfString:highlightedPattern options:NSCaseInsensitiveSearch];
if (range.location != NSNotFound)
{
customAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedTextMessage];
while (range.location != NSNotFound)
{
if (highlightedPatternColor)
{
// Update text color
[customAttributedTextMsg addAttribute:NSForegroundColorAttributeName value:highlightedPatternColor range:range];
}
if (highlightedPatternFont)
{
// Update text font
[customAttributedTextMsg addAttribute:NSFontAttributeName value:highlightedPatternFont range:range];
}
// Look for the next pattern occurrence
range.location += range.length;
if (range.location < currentTextMessage.length)
{
range.length = currentTextMessage.length - range.location;
range = [currentTextMessage rangeOfString:highlightedPattern options:NSCaseInsensitiveSearch range:range];
}
else
{
range.location = NSNotFound;
}
}
}
if (customAttributedTextMsg)
{
// Update resulting message body
attributedTextMessage = customAttributedTextMsg;
}
}
- (void)didMXSessionUpdatePublicisedGroupsForUsers:(NSNotification *)notif
{
// Retrieved the list of the concerned users
NSArray<NSString*> *userIds = notif.userInfo[kMXSessionNotificationUserIdsArrayKey];
if (userIds.count && self.senderId)
{
// Check whether the current sender is concerned.
if ([userIds indexOfObject:self.senderId] != NSNotFound)
{
[self refreshSenderFlair];
}
}
}
@end
@@ -0,0 +1,348 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <MatrixSDK/MatrixSDK.h>
#import "MXKRoomDataSource.h"
#import "MXKAttachment.h"
#import "MXEvent+MatrixKit.h"
@class MXKRoomDataSource;
/**
`MXKRoomBubbleCellDataStoring` defines a protocol a class must conform in order to store MXKRoomBubble cell data
managed by `MXKRoomDataSource`.
*/
@protocol MXKRoomBubbleCellDataStoring <NSObject>
#pragma mark - Data displayed by a room bubble cell
/**
The sender Id
*/
@property (nonatomic) NSString *senderId;
/**
The target Id (may be nil)
@discussion "target" refers to the room member who is the target of this event (if any), e.g.
the invitee, the person being banned, etc.
*/
@property (nonatomic) NSString *targetId;
/**
The room id
*/
@property (nonatomic) NSString *roomId;
/**
The sender display name composed when event occured
*/
@property (nonatomic) NSString *senderDisplayName;
/**
The sender avatar url retrieved when event occured
*/
@property (nonatomic) NSString *senderAvatarUrl;
/**
The sender avatar placeholder (may be nil) - Used when url is nil, or during avatar download.
*/
@property (nonatomic) UIImage *senderAvatarPlaceholder;
/**
The target display name composed when event occured (may be nil)
@discussion "target" refers to the room member who is the target of this event (if any), e.g.
the invitee, the person being banned, etc.
*/
@property (nonatomic) NSString *targetDisplayName;
/**
The target avatar url retrieved when event occured (may be nil)
@discussion "target" refers to the room member who is the target of this event (if any), e.g.
the invitee, the person being banned, etc.
*/
@property (nonatomic) NSString *targetAvatarUrl;
/**
The target avatar placeholder (may be nil) - Used when url is nil, or during avatar download.
@discussion "target" refers to the room member who is the target of this event (if any), e.g.
the invitee, the person being banned, etc.
*/
@property (nonatomic) UIImage *targetAvatarPlaceholder;
/**
The current sender flair (list of the publicised groups in the sender profile which matches the room flair settings)
*/
@property (nonatomic) NSArray<MXGroup*> *senderFlair;
/**
Tell whether the room is encrypted.
*/
@property (nonatomic) BOOL isEncryptedRoom;
/**
Tell whether a new pagination starts with this bubble.
*/
@property (nonatomic) BOOL isPaginationFirstBubble;
/**
Tell whether the sender information is relevant for this bubble
(For example this information should be hidden in case of 2 consecutive bubbles from the same sender).
*/
@property (nonatomic) BOOL shouldHideSenderInformation;
/**
Tell whether this bubble has nothing to display (neither a message nor an attachment).
*/
@property (nonatomic, readonly) BOOL hasNoDisplay;
/**
The list of events (`MXEvent` instances) handled by this bubble.
*/
@property (nonatomic, readonly) NSArray<MXEvent*> *events;
/**
The bubble attachment (if any).
*/
@property (nonatomic) MXKAttachment *attachment;
/**
The bubble date
*/
@property (nonatomic) NSDate *date;
/**
YES when the bubble is composed by incoming event(s).
*/
@property (nonatomic) BOOL isIncoming;
/**
YES when the bubble correspond to an attachment displayed with a thumbnail (see image, video).
*/
@property (nonatomic) BOOL isAttachmentWithThumbnail;
/**
YES when the bubble correspond to an attachment displayed with an icon (audio, file...).
*/
@property (nonatomic) BOOL isAttachmentWithIcon;
/**
Flag that indicates that self.attributedTextMessage will be not nil.
This avoids the computation of self.attributedTextMessage that can take time.
*/
@property (nonatomic, readonly) BOOL hasAttributedTextMessage;
/**
The body of the message with sets of attributes, or kind of content description in case of attachment (e.g. "image attachment")
*/
@property (nonatomic) NSAttributedString *attributedTextMessage;
/**
The raw text message (without attributes)
*/
@property (nonatomic) NSString *textMessage;
/**
Tell whether the sender's name is relevant or not for this bubble.
Return YES if the first component of the bubble message corresponds to an emote, or a state event in which
the sender's name appears at the beginning of the message text (for example membership events).
*/
@property (nonatomic) BOOL shouldHideSenderName;
/**
YES if the sender is currently typing in the current room
*/
@property (nonatomic) BOOL isTyping;
/**
Show the date time label in rendered bubble cell. NO by default.
*/
@property (nonatomic) BOOL showBubbleDateTime;
/**
A Boolean value that determines whether the date time labels are customized (By default date time display is handled by MatrixKit). NO by default.
*/
@property (nonatomic) BOOL useCustomDateTimeLabel;
/**
Show the receipts in rendered bubble cell. YES by default.
*/
@property (nonatomic) BOOL showBubbleReceipts;
/**
A Boolean value that determines whether the read receipts are customized (By default read receipts display is handled by MatrixKit). NO by default.
*/
@property (nonatomic) BOOL useCustomReceipts;
/**
A Boolean value that determines whether the unsent button is customized (By default an 'Unsent' button is displayed by MatrixKit in front of unsent events). NO by default.
*/
@property (nonatomic) BOOL useCustomUnsentButton;
/**
An integer that you can use to identify cell data in your application.
The default value is 0. You can set the value of this tag and use that value to identify the cell data later.
*/
@property (nonatomic) NSInteger tag;
/**
Indicate if antivirus scan status should be shown.
*/
@property (nonatomic, readonly) BOOL showAntivirusScanStatus;
#pragma mark - Public methods
/**
Create a new `MXKRoomBubbleCellDataStoring` object for a new bubble cell.
@param event the event to be displayed in the cell.
@param roomState the room state when the event occured.
@param roomDataSource the `MXKRoomDataSource` object that will use this instance.
@return the newly created instance.
*/
- (instancetype)initWithEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState andRoomDataSource:(MXKRoomDataSource*)roomDataSource;
/**
Update the event because its sent state changed or it is has been redacted.
@param eventId the id of the event to change.
@param event the new event data
@return the number of events hosting by the object after the update.
*/
- (NSUInteger)updateEvent:(NSString*)eventId withEvent:(MXEvent*)event;
/**
Remove the event from the `MXKRoomBubbleCellDataStoring` object.
@param eventId the id of the event to remove.
@return the number of events still hosting by the object after the removal
*/
- (NSUInteger)removeEvent:(NSString*)eventId;
/**
Remove the passed event and all events after it.
@param eventId the id of the event where to start removing.
@param removedEvents removedEvents will contain the list of removed events.
@return the number of events still hosting by the object after the removal.
*/
- (NSUInteger)removeEventsFromEvent:(NSString*)eventId removedEvents:(NSArray<MXEvent*>**)removedEvents;
/**
Check if the receiver has the same sender as another bubble.
@param bubbleCellData an object conforms to `MXKRoomBubbleCellDataStoring` protocol.
@return YES if the receiver has the same sender as the provided bubble
*/
- (BOOL)hasSameSenderAsBubbleCellData:(id<MXKRoomBubbleCellDataStoring>)bubbleCellData;
/**
Highlight text message of an event in the resulting message body.
@param eventId the id of the event to highlight.
@param tintColor optional tint color
@return The body of the message by highlighting the content related to the provided event id
*/
- (NSAttributedString*)attributedTextMessageWithHighlightedEvent:(NSString*)eventId tintColor:(UIColor*)tintColor;
/**
Highlight all the occurrences of a pattern in the resulting message body 'attributedTextMessage'.
@param pattern the text pattern to highlight.
@param patternColor optional text color (the pattern text color is unchanged if nil).
@param patternFont optional text font (the pattern font is unchanged if nil).
*/
- (void)highlightPatternInTextMessage:(NSString*)pattern withForegroundColor:(UIColor*)patternColor andFont:(UIFont*)patternFont;
/**
Refresh the sender flair information
*/
- (void)refreshSenderFlair;
/**
Indicate that the current text message layout is no longer valid and should be recomputed
before presentation in a bubble cell. This could be due to the content changing, or the
available space for the cell has been updated.
*/
- (void)invalidateTextLayout;
#pragma mark - Bubble collapsing
/**
A Boolean value that indicates if the cell is collapsable.
*/
@property (nonatomic) BOOL collapsable;
/**
A Boolean value that indicates if the cell and its series is collapsed.
*/
@property (nonatomic) BOOL collapsed;
/**
The attributed string to display when the collapsable cells series is collapsed.
It is not nil only for the start cell of the cells series.
*/
@property (nonatomic) NSAttributedString *collapsedAttributedTextMessage;
/**
Bidirectional linked list of cells that can be collapsed together.
If prevCollapsableCellData is nil, this cell data instance is the data of the start
cell of the collapsable cells series.
*/
@property (nonatomic) id<MXKRoomBubbleCellDataStoring> prevCollapsableCellData;
@property (nonatomic) id<MXKRoomBubbleCellDataStoring> nextCollapsableCellData;
/**
The room state to use for computing or updating the data to display for the series when it is
collapsed.
It is not nil only for the start cell of the cells series.
*/
@property (nonatomic) MXRoomState *collapseState;
/**
Check whether the two cells can be collapsable together.
@return YES if YES.
*/
- (BOOL)collapseWith:(id<MXKRoomBubbleCellDataStoring>)cellData;
@optional
/**
Attempt to add a new event to the bubble.
@param event the event to be displayed in the cell.
@param roomState the room state when the event occured.
@return YES if the model accepts that the event can concatenated to events already in the bubble.
*/
- (BOOL)addEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState;
/**
The receiver appends to its content the provided bubble cell data, if both have the same sender.
@param bubbleCellData an object conforms to `MXKRoomBubbleCellDataStoring` protocol.
@return YES if the provided cell data has been merged into receiver.
*/
- (BOOL)mergeWithBubbleCellData:(id<MXKRoomBubbleCellDataStoring>)bubbleCellData;
@end
@@ -0,0 +1,45 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKRoomBubbleCellData.h"
/**
`MXKRoomBubbleCellDataWithAppendingMode` class inherits from `MXKRoomBubbleCellData`, it merges
consecutive events from the same sender into one bubble.
Each concatenated event is represented by a bubble component.
*/
@interface MXKRoomBubbleCellDataWithAppendingMode : MXKRoomBubbleCellData
{
@protected
/**
YES if position of each component must be refreshed
*/
BOOL shouldUpdateComponentsPosition;
}
/**
The string appended to the current message before adding a new component text.
*/
+ (NSAttributedString *)messageSeparator;
/**
The maximum number of components in each bubble. Default is 10.
We limit the number of components to reduce the computation time required during bubble handling.
Indeed some process like [prepareBubbleComponentsPosition] is time consuming.
*/
@property (nonatomic) NSUInteger maxComponentCount;
@end
@@ -0,0 +1,356 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKRoomBubbleCellDataWithAppendingMode.h"
static NSAttributedString *messageSeparator = nil;
@implementation MXKRoomBubbleCellDataWithAppendingMode
#pragma mark - MXKRoomBubbleCellDataStoring
- (instancetype)initWithEvent:(MXEvent *)event andRoomState:(MXRoomState *)roomState andRoomDataSource:(MXKRoomDataSource *)roomDataSource2
{
self = [super initWithEvent:event andRoomState:roomState andRoomDataSource:roomDataSource2];
if (self)
{
// Set default settings
self.maxComponentCount = 10;
}
return self;
}
- (BOOL)addEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState
{
// We group together text messages from the same user (attachments are not merged).
if ([event.sender isEqualToString:self.senderId] && (self.attachment == nil) && (self.bubbleComponents.count < self.maxComponentCount))
{
// Attachments (image, video, sticker ...) cannot be added here
if ([roomDataSource.eventFormatter isSupportedAttachment:event])
{
return NO;
}
// Check sender information
NSString *eventSenderName = [roomDataSource.eventFormatter senderDisplayNameForEvent:event withRoomState:roomState];
NSString *eventSenderAvatar = [roomDataSource.eventFormatter senderAvatarUrlForEvent:event withRoomState:roomState];
if ((self.senderDisplayName || eventSenderName) &&
([self.senderDisplayName isEqualToString:eventSenderName] == NO))
{
return NO;
}
if ((self.senderAvatarUrl || eventSenderAvatar) &&
([self.senderAvatarUrl isEqualToString:eventSenderAvatar] == NO))
{
return NO;
}
// Take into account here the rendered bubbles pagination
if (roomDataSource.bubblesPagination == MXKRoomDataSourceBubblesPaginationPerDay)
{
// Event must be sent the same day than the existing bubble.
NSString *bubbleDateString = [roomDataSource.eventFormatter dateStringFromDate:self.date withTime:NO];
NSString *eventDateString = [roomDataSource.eventFormatter dateStringFromEvent:event withTime:NO];
if (bubbleDateString && eventDateString && ![bubbleDateString isEqualToString:eventDateString])
{
return NO;
}
}
// Create new message component
MXKRoomBubbleComponent *addedComponent = [[MXKRoomBubbleComponent alloc] initWithEvent:event roomState:roomState eventFormatter:roomDataSource.eventFormatter session:self.mxSession];
if (addedComponent)
{
[self addComponent:addedComponent];
}
// else the event is ignored, we consider it as handled
return YES;
}
return NO;
}
- (BOOL)mergeWithBubbleCellData:(id<MXKRoomBubbleCellDataStoring>)bubbleCellData
{
if ([self hasSameSenderAsBubbleCellData:bubbleCellData])
{
MXKRoomBubbleCellData *cellData = (MXKRoomBubbleCellData*)bubbleCellData;
// Only text messages are merged (Attachments are not merged).
if ((self.attachment == nil) && (cellData.attachment == nil))
{
// Take into account here the rendered bubbles pagination
if (roomDataSource.bubblesPagination == MXKRoomDataSourceBubblesPaginationPerDay)
{
// bubble components must be sent the same day than self.
NSString *selfDateString = [roomDataSource.eventFormatter dateStringFromDate:self.date withTime:NO];
NSString *bubbleDateString = [roomDataSource.eventFormatter dateStringFromDate:bubbleCellData.date withTime:NO];
if (![bubbleDateString isEqualToString:selfDateString])
{
return NO;
}
}
// Add all components of the provided message
for (MXKRoomBubbleComponent* component in cellData.bubbleComponents)
{
[self addComponent:component];
}
return YES;
}
}
return NO;
}
- (NSAttributedString*)attributedTextMessageWithHighlightedEvent:(NSString*)eventId tintColor:(UIColor*)tintColor
{
// Create attributed string
NSMutableAttributedString *customAttributedTextMsg;
NSAttributedString *componentString;
@synchronized(bubbleComponents)
{
for (MXKRoomBubbleComponent* component in bubbleComponents)
{
componentString = component.attributedTextMessage;
if (componentString)
{
if ([component.event.eventId isEqualToString:eventId])
{
NSMutableAttributedString *customComponentString = [[NSMutableAttributedString alloc] initWithAttributedString:componentString];
UIColor *color = tintColor ? tintColor : [UIColor lightGrayColor];
[customComponentString addAttribute:NSBackgroundColorAttributeName value:color range:NSMakeRange(0, customComponentString.length)];
componentString = customComponentString;
}
if (!customAttributedTextMsg)
{
customAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:componentString];
}
else
{
// Append attributed text
[customAttributedTextMsg appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]];
[customAttributedTextMsg appendAttributedString:componentString];
}
}
}
}
return customAttributedTextMsg;
}
#pragma mark -
- (void)prepareBubbleComponentsPosition
{
// Set position of the first component
[super prepareBubbleComponentsPosition];
@synchronized(bubbleComponents)
{
// Check whether the position of other components need to be refreshed
if (!self.attachment && shouldUpdateComponentsPosition && bubbleComponents.count > 1)
{
// Init attributed string with the first text component not nil.
MXKRoomBubbleComponent *component = bubbleComponents.firstObject;
CGFloat positionY = component.position.y;
NSMutableAttributedString *attributedString;
NSUInteger index = 0;
for (; index < bubbleComponents.count; index++)
{
component = [bubbleComponents objectAtIndex:index];
component.position = CGPointMake(0, positionY);
if (component.attributedTextMessage)
{
attributedString = [[NSMutableAttributedString alloc] initWithAttributedString:component.attributedTextMessage];
[attributedString appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]];
break;
}
}
for (index++; index < bubbleComponents.count; index++)
{
// Append the next text component
component = [bubbleComponents objectAtIndex:index];
if (component.attributedTextMessage)
{
[attributedString appendAttributedString:component.attributedTextMessage];
// Compute the height of the resulting string
CGFloat cumulatedHeight = [self rawTextHeight:attributedString];
// Deduce the position of the beginning of this component
CGFloat positionY = MXKROOMBUBBLECELLDATA_TEXTVIEW_DEFAULT_VERTICAL_INSET + (cumulatedHeight - [self rawTextHeight:component.attributedTextMessage]);
component.position = CGPointMake(0, positionY);
[attributedString appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]];
}
else
{
// Apply the current vertical position on this empty component.
component.position = CGPointMake(0, positionY);
}
}
}
}
shouldUpdateComponentsPosition = NO;
}
#pragma mark -
- (NSString*)textMessage
{
NSString *rawText = nil;
if (self.attributedTextMessage)
{
// Append all components text message
NSMutableString *currentTextMsg;
@synchronized(bubbleComponents)
{
for (MXKRoomBubbleComponent* component in bubbleComponents)
{
if (component.textMessage == nil)
{
continue;
}
if (!currentTextMsg)
{
currentTextMsg = [NSMutableString stringWithString:component.textMessage];
}
else
{
// Append text message
[currentTextMsg appendString:@"\n"];
[currentTextMsg appendString:component.textMessage];
}
}
}
rawText = currentTextMsg;
}
return rawText;
}
- (void)setAttributedTextMessage:(NSAttributedString *)inAttributedTextMessage
{
super.attributedTextMessage = inAttributedTextMessage;
// Position of each components should be computed again
shouldUpdateComponentsPosition = YES;
}
- (NSAttributedString*)attributedTextMessage
{
@synchronized(bubbleComponents)
{
if (self.hasAttributedTextMessage && !attributedTextMessage.length)
{
// Create attributed string
NSMutableAttributedString *currentAttributedTextMsg;
for (MXKRoomBubbleComponent* component in bubbleComponents)
{
if (component.attributedTextMessage)
{
if (!currentAttributedTextMsg)
{
currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:component.attributedTextMessage];
}
else
{
// Append attributed text
[currentAttributedTextMsg appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]];
[currentAttributedTextMsg appendAttributedString:component.attributedTextMessage];
}
}
}
self.attributedTextMessage = currentAttributedTextMsg;
}
}
return attributedTextMessage;
}
- (void)setMaxTextViewWidth:(CGFloat)inMaxTextViewWidth
{
CGFloat previousMaxWidth = self.maxTextViewWidth;
[super setMaxTextViewWidth:inMaxTextViewWidth];
// Check change
if (previousMaxWidth != self.maxTextViewWidth)
{
// Position of each components should be computed again
shouldUpdateComponentsPosition = YES;
}
}
#pragma mark -
+ (NSAttributedString *)messageSeparator
{
@synchronized(self)
{
if(messageSeparator == nil)
{
messageSeparator = [[NSAttributedString alloc] initWithString:@"\n\n" attributes:@{NSForegroundColorAttributeName : [UIColor blackColor],
NSFontAttributeName: [UIFont systemFontOfSize:4]}];
}
}
return messageSeparator;
}
#pragma mark - Privates
- (void)addComponent:(MXKRoomBubbleComponent*)addedComponent
{
@synchronized(bubbleComponents)
{
// Check date of existing components to insert this new one
NSUInteger index = bubbleComponents.count;
// Component without date is added at the end by default
if (addedComponent.date)
{
while (index)
{
MXKRoomBubbleComponent *msgComponent = [bubbleComponents objectAtIndex:(--index)];
if (msgComponent.date && [msgComponent.date compare:addedComponent.date] != NSOrderedDescending)
{
// New component will be inserted here
index ++;
break;
}
}
}
// Insert new component
[bubbleComponents insertObject:addedComponent atIndex:index];
// Indicate that the data's text message layout should be recomputed.
[self invalidateTextLayout];
}
}
@end
@@ -0,0 +1,27 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKRoomBubbleCellDataWithAppendingMode.h"
/**
`MXKRoomBubbleCellDataWithIncomingAppendingMode` class inherits from `MXKRoomBubbleCellDataWithAppendingMode`,
only the incoming message cells are merged.
*/
@interface MXKRoomBubbleCellDataWithIncomingAppendingMode : MXKRoomBubbleCellDataWithAppendingMode
{
}
@end
@@ -0,0 +1,45 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKRoomBubbleCellDataWithIncomingAppendingMode.h"
@implementation MXKRoomBubbleCellDataWithIncomingAppendingMode
#pragma mark - MXKRoomBubbleCellDataStoring
- (BOOL)addEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState
{
// Do not merge outgoing events
if ([event.sender isEqualToString:roomDataSource.mxSession.myUser.userId])
{
return NO;
}
return [super addEvent:event andRoomState:roomState];
}
- (BOOL)mergeWithBubbleCellData:(id<MXKRoomBubbleCellDataStoring>)bubbleCellData
{
// Do not merge outgoing events
if ([bubbleCellData.senderId isEqualToString:roomDataSource.mxSession.myUser.userId])
{
return NO;
}
return [super mergeWithBubbleCellData:bubbleCellData];
}
@end
@@ -0,0 +1,127 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <MatrixSDK/MatrixSDK.h>
#import "MXKEventFormatter.h"
#import "MXKURLPreviewDataProtocol.h"
/**
Flags to indicate if a fix is required at the display time.
*/
typedef enum : NSUInteger {
/**
No fix required.
*/
MXKRoomBubbleComponentDisplayFixNone = 0,
/**
Borders for HTML blockquotes need to be fixed.
*/
MXKRoomBubbleComponentDisplayFixHtmlBlockquote = 0x1
} MXKRoomBubbleComponentDisplayFix;
/**
`MXKRoomBubbleComponent` class compose data related to one `MXEvent` instance.
*/
@interface MXKRoomBubbleComponent : NSObject
/**
The body of the message, or kind of content description in case of attachment (e.g. "image attachment").
*/
@property (nonatomic) NSString *textMessage;
/**
The `textMessage` with sets of attributes.
*/
@property (nonatomic) NSAttributedString *attributedTextMessage;
/**
The event date
*/
@property (nonatomic) NSDate *date;
/**
Event formatter
*/
@property (nonatomic) MXKEventFormatter *eventFormatter;
/**
The event on which the component is based (used in case of redaction)
*/
@property (nonatomic, readonly) MXEvent *event;
// The following properties are defined to store information on component.
// They must be handled by the object which creates the MXKRoomBubbleComponent instance.
//@property (nonatomic) CGFloat height;
@property (nonatomic) CGPoint position;
/**
Set of flags indicating fixes that need to be applied at display time.
*/
@property (nonatomic) MXKRoomBubbleComponentDisplayFix displayFix;
/**
The first link detected in the event's content, otherwise nil.
*/
@property (nonatomic) NSURL *link;
/**
Any data necessary to show a URL preview.
Note: MatrixKit is unable to display this data by itself.
*/
@property (nonatomic) id <MXKURLPreviewDataProtocol> urlPreviewData;
/**
Whether a URL preview should be displayed for this cell.
Note: MatrixKit is unable to display URL previews by itself.
*/
@property (nonatomic) BOOL showURLPreview;
/**
Event antivirus scan. Present only if antivirus is enabled and event contains media.
*/
@property (nonatomic) MXEventScan *eventScan;
/**
Indicate if an encryption badge should be shown.
*/
@property (nonatomic, readonly) BOOL showEncryptionBadge;
/**
Create a new `MXKRoomBubbleComponent` object based on a `MXEvent` instance.
@param event the event used to compose the bubble component.
@param roomState the room state when the event occured.
@param eventFormatter object used to format event into displayable string.
@param session the related matrix session.
@return the newly created instance.
*/
- (instancetype)initWithEvent:(MXEvent*)event roomState:(MXRoomState*)roomState eventFormatter:(MXKEventFormatter*)eventFormatter session:(MXSession*)session;
/**
Update the event because its sent state changed or it is has been redacted.
@param event the new event data.
@param roomState the up-to-date state of the room.
@param session the related matrix session.
*/
- (void)updateWithEvent:(MXEvent*)event roomState:(MXRoomState*)roomState session:(MXSession*)session;
@end
@@ -0,0 +1,189 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKRoomBubbleComponent.h"
#import "MXEvent+MatrixKit.h"
#import "MXKSwiftHeader.h"
@implementation MXKRoomBubbleComponent
- (instancetype)initWithEvent:(MXEvent*)event roomState:(MXRoomState*)roomState eventFormatter:(MXKEventFormatter*)eventFormatter session:(MXSession*)session;
{
if (self = [super init])
{
// Build text component related to this event
_eventFormatter = eventFormatter;
MXKEventFormatterError error;
NSAttributedString *eventString = [_eventFormatter attributedStringFromEvent:event withRoomState:roomState error:&error];
// Store the potential error
event.mxkEventFormatterError = error;
_textMessage = nil;
_attributedTextMessage = eventString;
// Set date time
if (event.originServerTs != kMXUndefinedTimestamp)
{
_date = [NSDate dateWithTimeIntervalSince1970:(double)event.originServerTs/1000];
}
else
{
_date = nil;
}
// Keep ref on event (used to handle the read marker, or a potential event redaction).
_event = event;
_displayFix = MXKRoomBubbleComponentDisplayFixNone;
if ([event.content[@"format"] isEqualToString:kMXRoomMessageFormatHTML])
{
if ([((NSString*)event.content[@"formatted_body"]) containsString:@"<blockquote"])
{
_displayFix |= MXKRoomBubbleComponentDisplayFixHtmlBlockquote;
}
}
_showEncryptionBadge = [self shouldShowWarningBadgeForEvent:event roomState:(MXRoomState*)roomState session:session];
[self updateLinkWithRoomState:roomState];
}
return self;
}
- (void)updateWithEvent:(MXEvent*)event roomState:(MXRoomState*)roomState session:(MXSession*)session
{
// Report the new event
_event = event;
if (_event.isRedactedEvent)
{
// Do not use the live room state for redacted events as they occurred in the past
// Note: as we don't have valid room state in this case, userId will be used as display name
roomState = nil;
}
// Other calls to updateWithEvent are made to update the state of an event (ex: MXKEventStateSending to MXKEventStateDefault).
// They occur in live so we can use the room up-to-date state without making huge errors
_textMessage = nil;
MXKEventFormatterError error;
_attributedTextMessage = [_eventFormatter attributedStringFromEvent:event withRoomState:roomState error:&error];
_showEncryptionBadge = [self shouldShowWarningBadgeForEvent:event roomState:roomState session:session];
[self updateLinkWithRoomState:roomState];
}
- (NSString *)textMessage
{
if (!_textMessage)
{
_textMessage = _attributedTextMessage.string;
}
return _textMessage;
}
- (void)updateLinkWithRoomState:(MXRoomState*)roomState
{
// Ensure link detection has been enabled
if (!MXKAppSettings.standardAppSettings.enableBubbleComponentLinkDetection)
{
return;
}
// Only detect links in unencrypted rooms, for un-redacted message events that are text, notice or emote.
// Specifically check the room's encryption state rather than the event's as outgoing events are always unencrypted initially.
if (roomState.isEncrypted || self.event.eventType != MXEventTypeRoomMessage || [self.event isRedactedEvent])
{
self.link = nil; // Ensure there's no link for a redacted event
return;
}
NSString *messageType = self.event.content[@"msgtype"];
if (!messageType || !([messageType isEqualToString:kMXMessageTypeText] || [messageType isEqualToString:kMXMessageTypeNotice] || [messageType isEqualToString:kMXMessageTypeEmote]))
{
return;
}
// Detect links in the attributed string which gets updated when the message is edited.
// Restrict detection to the unquoted string so links are only found in the sender's message.
NSString *body = [self.attributedTextMessage mxk_unquotedString];
NSURL *url = [body mxk_firstURLDetected];
if (!url)
{
self.link = nil;
return;
}
self.link = url;
}
- (BOOL)shouldShowWarningBadgeForEvent:(MXEvent*)event roomState:(MXRoomState*)roomState session:(MXSession*)session
{
// Warning badges are unnecessary in unencrypted rooms
if (!roomState.isEncrypted)
{
return NO;
}
// Not all events are encrypted (e.g. state/reactions/redactions) and we only have encrypted cell subclasses for messages and attachments.
if (event.eventType != MXEventTypeRoomMessage && !event.isMediaAttachment)
{
return NO;
}
// Always show a warning badge if there was a decryption error.
if (event.decryptionError)
{
return YES;
}
// Unencrypted message events should show a warning unless they're pending local echoes
if (!event.isEncrypted)
{
if (event.isLocalEvent
|| event.contentHasBeenEdited) // Local echo for an edit is clear but uses a true event id, the one of the edited event
{
return NO;
}
return YES;
}
// The encryption is in a good state.
// Only show a warning badge if there are trust issues.
if (event.sender)
{
MXUserTrustLevel *userTrustLevel = [session.crypto trustLevelForUser:event.sender];
MXDeviceInfo *deviceInfo = [session.crypto eventDeviceInfo:event];
if (userTrustLevel.isVerified && !deviceInfo.trustLevel.isVerified)
{
return YES;
}
}
// Everything was fine
return NO;
}
@end
@@ -0,0 +1,78 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIImage.h>
#import <MatrixSDK/MXEnumConstants.h>
@class MXSession;
/**
`MXKRoomCreationInputs` objects lists all the fields considered for a new room creation.
*/
@interface MXKRoomCreationInputs : NSObject
/**
The selected matrix session in which the new room should be created.
*/
@property (nonatomic) MXSession* mxSession;
/**
The room name.
*/
@property (nonatomic) NSString* roomName;
/**
The room alias.
*/
@property (nonatomic) NSString* roomAlias;
/**
The room topic.
*/
@property (nonatomic) NSString* roomTopic;
/**
The room picture.
*/
@property (nonatomic) UIImage *roomPicture;
/**
The room visibility (kMXRoomVisibilityPrivate by default).
*/
@property (nonatomic) MXRoomDirectoryVisibility roomVisibility;
/**
The room participants (nil by default).
*/
@property (nonatomic) NSArray *roomParticipants;
/**
Add a participant.
@param participantId The matrix user id of the participant.
*/
- (void)addParticipant:(NSString *)participantId;
/**
Remove a participant.
@param participantId The matrix user id of the participant.
*/
- (void)removeParticipant:(NSString *)participantId;
@end
@@ -0,0 +1,74 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKRoomCreationInputs.h"
#import <MatrixSDK/MXSession.h>
@interface MXKRoomCreationInputs ()
{
NSMutableArray *participants;
}
@end
@implementation MXKRoomCreationInputs
- (instancetype)init
{
self = [super init];
if (self)
{
_roomVisibility = kMXRoomDirectoryVisibilityPrivate;
}
return self;
}
- (void)setRoomParticipants:(NSArray *)roomParticipants
{
participants = [NSMutableArray arrayWithArray:roomParticipants];
}
- (NSArray*)roomParticipants
{
return participants;
}
- (void)addParticipant:(NSString *)participantId
{
if (participantId.length)
{
if (!participants)
{
participants = [NSMutableArray array];
}
[participants addObject:participantId];
}
}
- (void)removeParticipant:(NSString *)participantId
{
if (participantId.length)
{
[participants removeObject:participantId];
if (!participants.count)
{
participants = nil;
}
}
}
@end
@@ -0,0 +1,779 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <UIKit/UIKit.h>
#import "MXKDataSource.h"
#import "MXKRoomBubbleCellDataStoring.h"
#import "MXKEventFormatter.h"
@class MXKQueuedEvent;
/**
Define the threshold which triggers a bubbles count flush.
*/
#define MXKROOMDATASOURCE_CACHED_BUBBLES_COUNT_THRESHOLD 30
/**
Define the number of messages to preload around the initial event.
*/
#define MXKROOMDATASOURCE_PAGINATION_LIMIT_AROUND_INITIAL_EVENT 30
/**
List the supported pagination of the rendered room bubble cells
*/
typedef enum : NSUInteger
{
/**
No pagination
*/
MXKRoomDataSourceBubblesPaginationNone,
/**
The rendered room bubble cells are paginated per day
*/
MXKRoomDataSourceBubblesPaginationPerDay
} MXKRoomDataSourceBubblesPagination;
#pragma mark - Cells identifiers
/**
String identifying the object used to store and prepare room bubble data.
*/
extern NSString *const kMXKRoomBubbleCellDataIdentifier;
#pragma mark - Notifications
/**
Posted when a server sync starts or ends (depend on 'serverSyncEventCount').
The notification object is the `MXKRoomDataSource` instance.
*/
extern NSString *const kMXKRoomDataSourceSyncStatusChanged;
/**
Posted when the data source has failed to paginate around an event.
The notification object is the `MXKRoomDataSource` instance. The `userInfo` dictionary contains the following key:
- kMXKRoomDataTimelineErrorErrorKey: The NSError.
*/
extern NSString *const kMXKRoomDataSourceTimelineError;
/**
Notifications `userInfo` keys
*/
extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey;
#pragma mark - MXKRoomDataSource
@protocol MXKRoomBubbleCellDataStoring;
@class MXKRoomBubbleCellData;
/**
The data source for `MXKRoomViewController`.
*/
@interface MXKRoomDataSource : MXKDataSource <UITableViewDataSource>
{
@protected
/**
The data for the cells served by `MXKRoomDataSource`.
*/
NSMutableArray<id<MXKRoomBubbleCellDataStoring>> *bubbles;
/**
The queue of events that need to be processed in order to compute their display.
*/
NSMutableArray<MXKQueuedEvent*> *eventsToProcess;
/**
The dictionary of the related groups that the current user did not join.
*/
NSMutableDictionary<NSString*, MXGroup*> *externalRelatedGroups;
}
/**
The id of the room managed by the data source.
*/
@property (nonatomic, readonly) NSString *roomId;
/**
The id of the secondary room managed by the data source. Events with specified types from the secondary room will be provided from the data source.
@see `secondaryRoomEventTypes`.
Can be nil.
*/
@property (nonatomic, copy) NSString *secondaryRoomId;
/**
Types of events to include from the secondary room. Default is all call events.
*/
@property (nonatomic, copy) NSArray<MXEventTypeString> *secondaryRoomEventTypes;
/**
The room the data comes from.
The object is defined when the MXSession has data for the room
*/
@property (nonatomic, readonly) MXRoom *room;
/**
The preloaded room.state.
*/
@property (nonatomic, readonly) MXRoomState *roomState;
/**
The timeline being managed. It can be the live timeline of the room
or a timeline from a past event, initialEventId.
*/
@property (nonatomic, readonly) MXEventTimeline *timeline;
/**
Flag indicating if the data source manages, or will manage, a live timeline.
*/
@property (nonatomic, readonly) BOOL isLive;
/**
Flag indicating if the data source is used to peek into a room, ie it gets data from
a room the user has not joined yet.
*/
@property (nonatomic, readonly) BOOL isPeeking;
/**
The list of the attachments with thumbnail in the current available bubbles (MXKAttachment instances).
Note: the stickers are excluded from the returned list.
Note2: the attachments for which the antivirus scan status is not available are excluded too.
*/
@property (nonatomic, readonly) NSArray *attachmentsWithThumbnail;
/**
The events are processed asynchronously. This property counts the number of queued events
during server sync for which the process is pending.
*/
@property (nonatomic, readonly) NSInteger serverSyncEventCount;
/**
The current text message partially typed in text input (use nil to reset it).
*/
@property (nonatomic) NSString *partialTextMessage;
#pragma mark - Configuration
/**
The text formatter applied on the events.
By default, the events are filtered according to the value stored in the shared application settings (see [MXKAppSettings standardAppSettings].eventsFilterForMessages).
The events whose the type doesn't belong to the this list are not displayed.
`MXKRoomBubbleCellDataStoring` instances can use it to format text.
*/
@property (nonatomic) MXKEventFormatter *eventFormatter;
/**
Show the date time label in rendered room bubble cells. NO by default.
*/
@property (nonatomic) BOOL showBubblesDateTime;
/**
A Boolean value that determines whether the date time labels are customized (By default date time display is handled by MatrixKit). NO by default.
*/
@property (nonatomic) BOOL useCustomDateTimeLabel;
/**
Show the read marker (if any) in the rendered room bubble cells. YES by default.
*/
@property (nonatomic) BOOL showReadMarker;
/**
Show the receipts in rendered bubble cell. YES by default.
*/
@property (nonatomic) BOOL showBubbleReceipts;
/**
A Boolean value that determines whether the read receipts are customized (By default read receipts display is handled by MatrixKit). NO by default.
*/
@property (nonatomic) BOOL useCustomReceipts;
/**
Show the reactions in rendered bubble cell. NO by default.
*/
@property (nonatomic) BOOL showReactions;
/**
Show only reactions with single Emoji. NO by default.
*/
@property (nonatomic) BOOL showOnlySingleEmojiReactions;
/**
A Boolean value that determines whether the unsent button is customized (By default an 'Unsent' button is displayed by MatrixKit in front of unsent events). NO by default.
*/
@property (nonatomic) BOOL useCustomUnsentButton;
/**
Show the typing notifications of other room members in the chat history (NO by default).
*/
@property (nonatomic) BOOL showTypingNotifications;
/**
The pagination applied on the rendered room bubble cells (MXKRoomDataSourceBubblesPaginationNone by default).
*/
@property (nonatomic) MXKRoomDataSourceBubblesPagination bubblesPagination;
/**
Max nbr of cached bubbles when there is no delegate.
The default value is 30.
*/
@property (nonatomic) unsigned long maxBackgroundCachedBubblesCount;
/**
The number of messages to preload around the initial event.
The default value is 30.
*/
@property (nonatomic) NSUInteger paginationLimitAroundInitialEvent;
/**
Tell whether only the message events with an url key in their content must be handled. NO by default.
Note: The stickers are not retained by this filter.
*/
@property (nonatomic) BOOL filterMessagesWithURL;
#pragma mark - Life cycle
/**
Asynchronously create a data source to serve data corresponding to the passed room.
This method preloads room data, like the room state, to make it available once
the room data source is created.
@param roomId the id of the room to get data from.
@param mxSession the Matrix session to get data from.
@param onComplete a block providing the newly created instance.
*/
+ (void)loadRoomDataSourceWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)mxSession onComplete:(void (^)(id roomDataSource))onComplete;
/**
Asynchronously create adata source to serve data corresponding to an event in the
past of a room.
This method preloads room data, like the room state, to make it available once
the room data source is created.
@param roomId the id of the room to get data from.
@param initialEventId the id of the event where to start the timeline.
@param mxSession the Matrix session to get data from.
@param onComplete a block providing the newly created instance.
*/
+ (void)loadRoomDataSourceWithRoomId:(NSString*)roomId initialEventId:(NSString*)initialEventId andMatrixSession:(MXSession*)mxSession onComplete:(void (^)(id roomDataSource))onComplete;
/**
Asynchronously create a data source to peek into a room.
The data source will close the `peekingRoom` instance on [self destroy].
This method preloads room data, like the room state, to make it available once
the room data source is created.
@param peekingRoom the room to peek.
@param initialEventId the id of the event where to start the timeline. nil means the live
timeline.
@param onComplete a block providing the newly created instance.
*/
+ (void)loadRoomDataSourceWithPeekingRoom:(MXPeekingRoom*)peekingRoom andInitialEventId:(NSString*)initialEventId onComplete:(void (^)(id roomDataSource))onComplete;
#pragma mark - Constructors (Should not be called directly)
/**
Initialise the data source to serve data corresponding to the passed room.
@param roomId the id of the room to get data from.
@param mxSession the Matrix session to get data from.
@return the newly created instance.
*/
- (instancetype)initWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)mxSession;
/**
Initialise the data source to serve data corresponding to an event in the
past of a room.
@param roomId the id of the room to get data from.
@param initialEventId the id of the event where to start the timeline.
@param mxSession the Matrix session to get data from.
@return the newly created instance.
*/
- (instancetype)initWithRoomId:(NSString*)roomId initialEventId:(NSString*)initialEventId andMatrixSession:(MXSession*)mxSession;
/**
Initialise the data source to peek into a room.
The data source will close the `peekingRoom` instance on [self destroy].
@param peekingRoom the room to peek.
@param initialEventId the id of the event where to start the timeline. nil means the live
timeline.
@return the newly created instance.
*/
- (instancetype)initWithPeekingRoom:(MXPeekingRoom*)peekingRoom andInitialEventId:(NSString*)initialEventId;
/**
Mark all messages as read in the room.
*/
- (void)markAllAsRead;
/**
Reduce memory usage by releasing room data if the number of bubbles is over the provided limit 'maxBubbleNb'.
This operation is ignored if some local echoes are pending or if unread messages counter is not nil.
@param maxBubbleNb The room bubble data are released only if the number of bubbles is over this limit.
*/
- (void)limitMemoryUsage:(NSInteger)maxBubbleNb;
/**
Force data reload.
*/
- (void)reload;
/**
Called when room property changed. Designed to be used by subclasses.
*/
- (void)roomDidSet;
#pragma mark - Public methods
/**
Get the data for the cell at the given index.
@param index the index of the cell in the array
@return the cell data
*/
- (id<MXKRoomBubbleCellDataStoring>)cellDataAtIndex:(NSInteger)index;
/**
Get the data for the cell which contains the event with the provided event id.
@param eventId the event identifier
@return the cell data
*/
- (id<MXKRoomBubbleCellDataStoring>)cellDataOfEventWithEventId:(NSString*)eventId;
/**
Get the index of the cell which contains the event with the provided event id.
@param eventId the event identifier
@return the index of the concerned cell (NSNotFound if none).
*/
- (NSInteger)indexOfCellDataWithEventId:(NSString *)eventId;
/**
Get height of the cell at the given index.
@param index the index of the cell in the array.
@param maxWidth the maximum available width.
@return the cell height (0 if no data is available for this cell, or if the delegate is undefined).
*/
- (CGFloat)cellHeightAtIndex:(NSInteger)index withMaximumWidth:(CGFloat)maxWidth;
/**
Force bubbles cell data message recalculation.
*/
- (void)invalidateBubblesCellDataCache;
#pragma mark - Pagination
/**
Load more messages.
This method fails (with nil error) if the data source is not ready (see `MXKDataSourceStateReady`).
@param numItems the number of items to get.
@param direction backwards or forwards.
@param onlyFromStore if YES, return available events from the store, do not make a pagination request to the homeserver.
@param success a block called when the operation succeeds. This block returns the number of added cells.
(Note this count may be 0 if paginated messages have been concatenated to the current first cell).
@param failure a block called when the operation fails.
*/
- (void)paginate:(NSUInteger)numItems direction:(MXTimelineDirection)direction onlyFromStore:(BOOL)onlyFromStore success:(void (^)(NSUInteger addedCellNumber))success failure:(void (^)(NSError *error))failure;
/**
Load enough messages to fill the rect.
This method fails (with nil error) if the data source is not ready (see `MXKDataSourceStateReady`),
or if the delegate is undefined (this delegate is required to compute the actual size of the cells).
@param rect the rect to fill.
@param direction backwards or forwards.
@param minRequestMessagesCount if messages are not available in the store, a request to the homeserver
is required. minRequestMessagesCount indicates the minimum messages count to retrieve from the hs.
@param success a block called when the operation succeeds.
@param failure a block called when the operation fails.
*/
- (void)paginateToFillRect:(CGRect)rect direction:(MXTimelineDirection)direction withMinRequestMessagesCount:(NSUInteger)minRequestMessagesCount success:(void (^)(void))success failure:(void (^)(NSError *error))failure;
#pragma mark - Sending
/**
Send a text message to the room.
While sending, a fake event will be echoed in the messages list.
Once complete, this local echo will be replaced by the event saved by the homeserver.
@param text the text to send.
@param success A block object called when the operation succeeds. It returns
the event id of the event generated on the homeserver
@param failure A block object called when the operation fails.
*/
- (void)sendTextMessage:(NSString*)text
success:(void (^)(NSString *eventId))success
failure:(void (^)(NSError *error))failure;
/**
Send a reply to an event with text message to the room.
While sending, a fake event will be echoed in the messages list.
Once complete, this local echo will be replaced by the event saved by the homeserver.
@param eventIdToReply the id of event to reply.
@param text the text to send.
@param success A block object called when the operation succeeds. It returns
the event id of the event generated on the homeserver
@param failure A block object called when the operation fails.
*/
- (void)sendReplyToEventWithId:(NSString*)eventIdToReply
withTextMessage:(NSString *)text
success:(void (^)(NSString *))success
failure:(void (^)(NSError *))failure;
/**
Indicates if replying to the provided event is supported.
Only event of type 'MXEventTypeRoomMessage' are supported for the moment, and for certain msgtype.
@param eventId The id of the event.
@return YES if it is possible to reply to this event.
*/
- (BOOL)canReplyToEventWithId:(NSString*)eventId;
/**
Send an image to the room.
While sending, a fake event will be echoed in the messages list.
Once complete, this local echo will be replaced by the event saved by the homeserver.
@param image the UIImage containing the image to send.
@param success A block object called when the operation succeeds. It returns
the event id of the event generated on the homeserver
@param failure A block object called when the operation fails.
*/
- (void)sendImage:(UIImage*)image
success:(void (^)(NSString *eventId))success
failure:(void (^)(NSError *error))failure;
/**
Send an image to the room.
While sending, a fake event will be echoed in the messages list.
Once complete, this local echo will be replaced by the event saved by the homeserver.
@param imageData the full-sized image data of the image to send.
@param mimetype the mime type of the image
@param success A block object called when the operation succeeds. It returns
the event id of the event generated on the homeserver
@param failure A block object called when the operation fails.
*/
- (void)sendImage:(NSData*)imageData mimeType:(NSString*)mimetype success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure;
/**
Send a video to the room.
While sending, a fake event will be echoed in the messages list.
Once complete, this local echo will be replaced by the event saved by the homeserver.
@param videoLocalURL the local filesystem path of the video to send.
@param videoThumbnail the UIImage hosting a video thumbnail.
@param success A block object called when the operation succeeds. It returns
the event id of the event generated on the homeserver
@param failure A block object called when the operation fails.
*/
- (void)sendVideo:(NSURL*)videoLocalURL
withThumbnail:(UIImage*)videoThumbnail
success:(void (^)(NSString *eventId))success
failure:(void (^)(NSError *error))failure;
/**
Send a video to the room.
While sending, a fake event will be echoed in the messages list.
Once complete, this local echo will be replaced by the event saved by the homeserver.
@param videoAsset the AVAsset that represents the video to send.
@param videoThumbnail the UIImage hosting a video thumbnail.
@param success A block object called when the operation succeeds. It returns
the event id of the event generated on the homeserver
@param failure A block object called when the operation fails.
*/
- (void)sendVideoAsset:(AVAsset*)videoAsset
withThumbnail:(UIImage*)videoThumbnail
success:(void (^)(NSString *eventId))success
failure:(void (^)(NSError *error))failure;
/**
Send an audio file to the room.
While sending, a fake event will be echoed in the messages list.
Once complete, this local echo will be replaced by the event saved by the homeserver.
@param audioFileLocalURL the local filesystem path of the audio file to send.
@param mimeType the mime type of the file.
@param success A block object called when the operation succeeds. It returns
the event id of the event generated on the homeserver
@param failure A block object called when the operation fails.
*/
- (void)sendAudioFile:(NSURL *)audioFileLocalURL
mimeType:mimeType
success:(void (^)(NSString *))success
failure:(void (^)(NSError *))failure;
/**
Send a voice message to the room.
While sending, a fake event will be echoed in the messages list.
Once complete, this local echo will be replaced by the event saved by the homeserver.
@param audioFileLocalURL the local filesystem path of the audio file to send.
@param mimeType (optional) the mime type of the file. Defaults to `audio/ogg`
@param duration the length of the voice message in milliseconds
@param samples an array of floating point values normalized to [0, 1], boxed within NSNumbers
@param success A block object called when the operation succeeds. It returns
the event id of the event generated on the homeserver
@param failure A block object called when the operation fails.
*/
- (void)sendVoiceMessage:(NSURL *)audioFileLocalURL
mimeType:mimeType
duration:(NSUInteger)duration
samples:(NSArray<NSNumber *> *)samples
success:(void (^)(NSString *))success
failure:(void (^)(NSError *))failure;
/**
Send a file to the room.
While sending, a fake event will be echoed in the messages list.
Once complete, this local echo will be replaced by the event saved by the homeserver.
@param fileLocalURL the local filesystem path of the file to send.
@param mimeType the mime type of the file.
@param success A block object called when the operation succeeds. It returns
the event id of the event generated on the homeserver
@param failure A block object called when the operation fails.
*/
- (void)sendFile:(NSURL*)fileLocalURL
mimeType:(NSString*)mimeType
success:(void (^)(NSString *eventId))success
failure:(void (^)(NSError *error))failure;
/**
Send a room message to a room.
While sending, a fake event will be echoed in the messages list.
Once complete, this local echo will be replaced by the event saved by the homeserver.
@param content the message content that will be sent to the server as a JSON object.
@param success A block object called when the operation succeeds. It returns
the event id of the event generated on the homeserver
@param failure A block object called when the operation fails.
*/
- (void)sendMessageWithContent:(NSDictionary*)content
success:(void (^)(NSString *eventId))success
failure:(void (^)(NSError *error))failure;
/**
Send a generic non state event to a room.
While sending, a fake event will be echoed in the messages list.
Once complete, this local echo will be replaced by the event saved by the homeserver.
@param eventTypeString the type of the event. @see MXEventType.
@param content the content that will be sent to the server as a JSON object.
@param success A block object called when the operation succeeds. It returns
the event id of the event generated on the homeserver
@param failure A block object called when the operation fails.
*/
- (void)sendEventOfType:(MXEventTypeString)eventTypeString
content:(NSDictionary<NSString*, id>*)content
success:(void (^)(NSString *eventId))success
failure:(void (^)(NSError *error))failure;
/**
Resend a room message event.
The echo message corresponding to the event will be removed and a new echo message
will be added at the end of the room history.
@param eventId of the event to resend.
@param success A block object called when the operation succeeds. It returns
the event id of the event generated on the homeserver
@param failure A block object called when the operation fails.
*/
- (void)resendEventWithEventId:(NSString*)eventId
success:(void (^)(NSString *eventId))success
failure:(void (^)(NSError *error))failure;
#pragma mark - Events management
/**
Get an event loaded in this room datasource.
@param eventId of the event to retrieve.
@return the MXEvent object or nil if not found.
*/
- (MXEvent *)eventWithEventId:(NSString *)eventId;
/**
Remove an event from the events loaded by room datasource.
@param eventId of the event to remove.
*/
- (void)removeEventWithEventId:(NSString *)eventId;
/**
This method is called for each read receipt event received in forward mode.
By default, it tells the delegate that some cell data/views have been changed.
You may override this method to handle the receipt event according to the application needs.
You should not call this method directly.
You may override it in inherited 'MXKRoomDataSource' class.
@param receiptEvent an event with 'm.receipt' type.
@param roomState the room state right before the event
*/
- (void)didReceiveReceiptEvent:(MXEvent *)receiptEvent roomState:(MXRoomState *)roomState;
/**
Update read receipts for an event in a bubble cell data.
@param cellData The cell data to update.
@param readReceipts The new read receipts.
@param eventId The id of the event.
*/
- (void)updateCellData:(MXKRoomBubbleCellData*)cellData withReadReceipts:(NSArray<MXReceiptData*>*)readReceipts forEventId:(NSString*)eventId;
/**
Overridable method to customise the way how unsent messages are managed.
By default, they are added to the end of the timeline.
*/
- (void)handleUnsentMessages;
#pragma mark - Asynchronous events processing
/**
The dispatch queue to process room messages.
This processing can consume time. Handling it on a separated thread avoids to block the main thread.
All MXKRoomDataSource instances share the same dispatch queue.
*/
+ (dispatch_queue_t)processingQueue;
#pragma mark - Bubble collapsing
/**
Collapse or expand a series of collapsable bubbles.
@param bubbleData the first bubble of the series.
@param collapsed YES to collapse. NO to expand.
*/
- (void)collapseRoomBubble:(id<MXKRoomBubbleCellDataStoring>)bubbleData collapsed:(BOOL)collapsed;
#pragma mark - Groups
/**
Get a MXGroup instance for a group.
This method is used by the bubble to retrieve a related groups of the room.
@param groupId The identifier to the group.
@return the MXGroup instance.
*/
- (MXGroup *)groupWithGroupId:(NSString*)groupId;
#pragma mark - Reactions
/**
Indicates if it's possible to react on the event.
@param eventId The id of the event.
@return True to indicates reaction possibility for this event.
*/
- (BOOL)canReactToEventWithId:(NSString*)eventId;
/**
Send a reaction to an event.
@param reaction Reaction to add.
@param eventId The id of the event.
@param success A block object called when the operation succeeds.
@param failure A block object called when the operation fails.
*/
- (void)addReaction:(NSString *)reaction
forEventId:(NSString *)eventId
success:(void (^)(void))success
failure:(void (^)(NSError *error))failure;
/**
Unreact a reaction to an event.
@param reaction Reaction to unreact.
@param eventId The id of the event.
@param success A block object called when the operation succeeds.
@param failure A block object called when the operation fails.
*/
- (void)removeReaction:(NSString *)reaction
forEventId:(NSString *)eventId
success:(void (^)(void))success
failure:(void (^)(NSError *error))failure;
#pragma mark - Editions
/**
Indicates if it's possible to edit the event content.
@param eventId The id of the event.
@return True to indicates edition possibility for this event.
*/
- (BOOL)canEditEventWithId:(NSString*)eventId;
/**
Replace a text in an event.
@param eventId The eventId of event to replace.
@param text The new message text.
@param success A block object called when the operation succeeds. It returns
the event id of the event generated on the homeserver.
@param failure A block object called when the operation fails.
*/
- (void)replaceTextMessageForEventWithId:(NSString *)eventId
withTextMessage:(NSString *)text
success:(void (^)(NSString *eventId))success
failure:(void (^)(NSError *error))failure;
/**
Update reactions for an event in a bubble cell data.
@param cellData The cell data to update.
@param eventId The id of the event.
*/
- (void)updateCellDataReactions:(id<MXKRoomBubbleCellDataStoring>)cellData forEventId:(NSString*)eventId;
/**
Retrieve editable text message from an event.
@param event An event.
@return Event text editable by user.
*/
- (NSString*)editableTextMessageForEvent:(MXEvent*)event;
@end
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,124 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "MXKRoomDataSource.h"
/**
`MXKRoomDataSourceManagerReleasePolicy` defines how a `MXKRoomDataSource` instance must be released
when [MXKRoomDataSourceManager closeRoomDataSourceWithRoomId:] is called.
Once released, the in-memory data (messages that are outgoing, failed sending, ...) of room data source
is lost.
*/
typedef enum : NSUInteger {
/**
Created `MXKRoomDataSource` instances are never released when they are closed.
*/
MXKRoomDataSourceManagerReleasePolicyNeverRelease,
/**
Created `MXKRoomDataSource` instances are released when they are closed.
*/
MXKRoomDataSourceManagerReleasePolicyReleaseOnClose,
} MXKRoomDataSourceManagerReleasePolicy;
/**
`MXKRoomDataSourceManager` manages a pool of `MXKRoomDataSource` instances for a given Matrix session.
It makes the `MXKRoomDataSource` instances reusable so that their data (messages that are outgoing, failed sending, ...)
is not lost when the view controller that displays them is gone.
*/
@interface MXKRoomDataSourceManager : NSObject
/**
Retrieve the MXKRoomDataSources manager for a particular Matrix session.
@param mxSession the Matrix session,
@return the MXKRoomDataSources manager to use for this session.
*/
+ (MXKRoomDataSourceManager*)sharedManagerForMatrixSession:(MXSession*)mxSession;
/**
Remove the MXKRoomDataSources manager for a particular Matrix session.
@param mxSession the Matrix session.
*/
+ (void)removeSharedManagerForMatrixSession:(MXSession*)mxSession;
/**
Register the MXKRoomDataSource-inherited class that will be used to instantiate all room data source.
By default MXKRoomDataSource class is considered.
CAUTION: All existing room data source instances are reset in case of class change.
@param roomDataSourceClass a MXKRoomDataSource-inherited class.
*/
+ (void)registerRoomDataSourceClass:(Class)roomDataSourceClass;
/**
Force close all the current room data source instances.
*/
- (void)reset;
/**
Get a room data source corresponding to a room id.
If a room data source already exists for this room, its reference will be returned. Else,
if requested, the method will instantiate it.
@param roomId the room id of the room.
@param create if YES, the MXKRoomDataSourceManager will create the room data source if it does not exist yet.
@param onComplete blocked with the room data source (instance of MXKRoomDataSource-inherited class).
*/
- (void)roomDataSourceForRoom:(NSString*)roomId create:(BOOL)create onComplete:(void (^)(MXKRoomDataSource *roomDataSource))onComplete;
/**
Make a room data source be managed by the manager.
Use this method to add a MXKRoomDataSource-inherited instance that cannot be automatically created by
[MXKRoomDataSourceManager roomDataSourceForRoom: create:].
@param roomDataSource the MXKRoomDataSource-inherited object to the manager scope.
*/
- (void)addRoomDataSource:(MXKRoomDataSource*)roomDataSource;
/**
Close the roomDataSource.
The roomDataSource instance will be actually destroyed according to the current release policy.
@param roomId the room if of the data source to release.
@param forceRelease if yes the room data source instance will be destroyed whatever the policy is.
*/
- (void)closeRoomDataSourceWithRoomId:(NSString*)roomId forceClose:(BOOL)forceRelease;
/**
The release policy to apply when `MXKRoomDataSource` instances are closed.
Default is MXKRoomDataSourceManagerReleasePolicyNeverRelease.
*/
@property (nonatomic) MXKRoomDataSourceManagerReleasePolicy releasePolicy;
/**
Tells whether a server sync is in progress in the matrix session.
*/
@property (nonatomic, readonly) BOOL isServerSyncInProgress;
@end
@@ -0,0 +1,271 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKRoomDataSourceManager.h"
@interface MXKRoomDataSourceManager()
{
MXSession *mxSession;
/**
The list of running roomDataSources.
Each key is a room ID. Each value, the MXKRoomDataSource instance.
*/
NSMutableDictionary *roomDataSources;
/**
Observe UIApplicationDidReceiveMemoryWarningNotification to dispose of any resources that can be recreated.
*/
id UIApplicationDidReceiveMemoryWarningNotificationObserver;
}
@end
static NSMutableDictionary *_roomDataSourceManagers = nil;
static Class _roomDataSourceClass;
@implementation MXKRoomDataSourceManager
+ (MXKRoomDataSourceManager *)sharedManagerForMatrixSession:(MXSession *)mxSession
{
// Manage a pool of managers: one per Matrix session
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_roomDataSourceManagers = [NSMutableDictionary dictionary];
});
MXKRoomDataSourceManager *roomDataSourceManager;
// Compute an id for this mxSession object: its pointer address as a string
NSString *mxSessionId = [NSString stringWithFormat:@"%p", mxSession];
@synchronized(_roomDataSourceManagers)
{
if (_roomDataSourceClass == nil)
{
// Set default class
_roomDataSourceClass = MXKRoomDataSource.class;
}
// If not available yet, create the `MXKRoomDataSourceManager` for this Matrix session
roomDataSourceManager = _roomDataSourceManagers[mxSessionId];
if (!roomDataSourceManager)
{
roomDataSourceManager = [[MXKRoomDataSourceManager alloc]initWithMatrixSession:mxSession];
_roomDataSourceManagers[mxSessionId] = roomDataSourceManager;
}
}
return roomDataSourceManager;
}
+ (void)removeSharedManagerForMatrixSession:(MXSession*)mxSession
{
// Compute the id for this mxSession object: its pointer address as a string
NSString *mxSessionId = [NSString stringWithFormat:@"%p", mxSession];
@synchronized(_roomDataSourceManagers)
{
MXKRoomDataSourceManager *roomDataSourceManager = [_roomDataSourceManagers objectForKey:mxSessionId];
if (roomDataSourceManager)
{
[roomDataSourceManager destroy];
[_roomDataSourceManagers removeObjectForKey:mxSessionId];
}
}
}
+ (void)registerRoomDataSourceClass:(Class)roomDataSourceClass
{
// Sanity check: accept only MXKRoomDataSource classes or sub-classes
NSParameterAssert([roomDataSourceClass isSubclassOfClass:MXKRoomDataSource.class]);
@synchronized(_roomDataSourceManagers)
{
if (roomDataSourceClass !=_roomDataSourceClass)
{
_roomDataSourceClass = roomDataSourceClass;
NSArray *mxSessionIds = _roomDataSourceManagers.allKeys;
for (NSString *mxSessionId in mxSessionIds)
{
MXKRoomDataSourceManager *roomDataSourceManager = [_roomDataSourceManagers objectForKey:mxSessionId];
if (roomDataSourceManager)
{
[roomDataSourceManager destroy];
[_roomDataSourceManagers removeObjectForKey:mxSessionId];
}
}
}
}
}
- (instancetype)initWithMatrixSession:(MXSession *)matrixSession
{
self = [super init];
if (self)
{
mxSession = matrixSession;
roomDataSources = [NSMutableDictionary dictionary];
_releasePolicy = MXKRoomDataSourceManagerReleasePolicyNeverRelease;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionDidLeaveRoom:) name:kMXSessionDidLeaveRoomNotification object:nil];
// Observe UIApplicationDidReceiveMemoryWarningNotification
UIApplicationDidReceiveMemoryWarningNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidReceiveMemoryWarningNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
MXLogDebug(@"[MXKRoomDataSourceManager] %@: Received memory warning.", self);
// Reload all data sources (except the current used ones) to reduce memory usage.
for (MXKRoomDataSource *roomDataSource in self->roomDataSources.allValues)
{
if (!roomDataSource.delegate)
{
[roomDataSource reload];
}
}
}];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidLeaveRoomNotification object:nil];
}
- (void)destroy
{
[self reset];
if (UIApplicationDidReceiveMemoryWarningNotificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:UIApplicationDidReceiveMemoryWarningNotificationObserver];
UIApplicationDidReceiveMemoryWarningNotificationObserver = nil;
}
}
#pragma mark
- (BOOL)isServerSyncInProgress
{
// Check first the matrix session state
if (mxSession.state == MXSessionStateSyncInProgress)
{
return YES;
}
// Check all data sources (events process is asynchronous, server sync may not be complete in data source).
for (MXKRoomDataSource *roomDataSource in roomDataSources.allValues)
{
if (roomDataSource.serverSyncEventCount)
{
return YES;
}
}
return NO;
}
#pragma mark
- (void)reset
{
NSArray *roomIds = roomDataSources.allKeys;
for (NSString *roomId in roomIds)
{
[self closeRoomDataSourceWithRoomId:roomId forceClose:YES];
}
}
- (void)roomDataSourceForRoom:(NSString *)roomId create:(BOOL)create onComplete:(void (^)(MXKRoomDataSource *roomDataSource))onComplete
{
NSParameterAssert(roomId);
// If not available yet, create the room data source
MXKRoomDataSource *roomDataSource = roomDataSources[roomId];
if (!roomDataSource && create && roomId)
{
[_roomDataSourceClass loadRoomDataSourceWithRoomId:roomId andMatrixSession:mxSession onComplete:^(id roomDataSource) {
[self addRoomDataSource:roomDataSource];
onComplete(roomDataSource);
}];
}
else
{
onComplete(roomDataSource);
}
}
- (void)addRoomDataSource:(MXKRoomDataSource *)roomDataSource
{
roomDataSources[roomDataSource.roomId] = roomDataSource;
}
- (void)closeRoomDataSourceWithRoomId:(NSString*)roomId forceClose:(BOOL)forceRelease;
{
// Check first whether this roomDataSource is well handled by this manager
if (!roomId || !roomDataSources[roomId])
{
MXLogDebug(@"[MXKRoomDataSourceManager] Failed to close an unknown room id: %@", roomId);
return;
}
MXKRoomDataSource *roomDataSource = roomDataSources[roomId];
// According to the policy, it is interesting to keep the room data source in life: it can keep managing echo messages
// in background for instance
MXKRoomDataSourceManagerReleasePolicy releasePolicy = _releasePolicy;
if (forceRelease)
{
// Act as ReleaseOnClose policy
releasePolicy = MXKRoomDataSourceManagerReleasePolicyReleaseOnClose;
}
switch (releasePolicy)
{
case MXKRoomDataSourceManagerReleasePolicyReleaseOnClose:
// Destroy and forget the instance
[roomDataSource destroy];
[roomDataSources removeObjectForKey:roomDataSource.roomId];
break;
case MXKRoomDataSourceManagerReleasePolicyNeverRelease:
// The close here consists in no more sending actions to the current view controller, the room data source delegate
roomDataSource.delegate = nil;
// Keep the instance for life (reduce memory usage by flushing room data if the number of bubbles is over 30).
[roomDataSource limitMemoryUsage:roomDataSource.maxBackgroundCachedBubblesCount];
break;
default:
break;
}
}
- (void)didMXSessionDidLeaveRoom:(NSNotification *)notif
{
if (mxSession == notif.object)
{
// The room is no more available, remove it from the manager
[self closeRoomDataSourceWithRoomId:notif.userInfo[kMXSessionNotificationRoomIdKey] forceClose:YES];
}
}
@end
@@ -0,0 +1,25 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <MatrixSDK/MXSendReplyEventStringLocalizerProtocol.h>
/**
A `MXKSendReplyEventStringLocalizer` instance represents string localizations used when send reply event to a message in a room.
*/
@interface MXKSendReplyEventStringLocalizer : NSObject<MXSendReplyEventStringLocalizerProtocol>
@end
@@ -0,0 +1,53 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKSendReplyEventStringLocalizer.h"
#import "MXKSwiftHeader.h"
@implementation MXKSendReplyEventStringLocalizer
- (NSString *)senderSentAnImage
{
return [MatrixKitL10n messageReplyToSenderSentAnImage];
}
- (NSString *)senderSentAVideo
{
return [MatrixKitL10n messageReplyToSenderSentAVideo];
}
- (NSString *)senderSentAnAudioFile
{
return [MatrixKitL10n messageReplyToSenderSentAnAudioFile];
}
- (NSString *)senderSentAVoiceMessage
{
return [MatrixKitL10n messageReplyToSenderSentAVoiceMessage];
}
- (NSString *)senderSentAFile
{
return [MatrixKitL10n messageReplyToSenderSentAFile];
}
- (NSString *)messageToReplyToPrefix
{
return [MatrixKitL10n messageReplyToMessageToReplyToPrefix];
}
@end
@@ -0,0 +1,33 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
@import Foundation;
/**
Slash commands used to perform actions from a room.
*/
FOUNDATION_EXPORT NSString *const kMXKSlashCmdChangeDisplayName;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdEmote;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdJoinRoom;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdPartRoom;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdInviteUser;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdKickUser;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdBanUser;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdUnbanUser;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdSetUserPowerLevel;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdResetUserPowerLevel;
FOUNDATION_EXPORT NSString *const kMXKSlashCmdChangeRoomTopic;
@@ -0,0 +1,29 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKSlashCommands.h"
NSString *const kMXKSlashCmdChangeDisplayName = @"/nick";
NSString *const kMXKSlashCmdEmote = @"/me";
NSString *const kMXKSlashCmdJoinRoom = @"/join";
NSString *const kMXKSlashCmdPartRoom = @"/part";
NSString *const kMXKSlashCmdInviteUser = @"/invite";
NSString *const kMXKSlashCmdKickUser = @"/kick";
NSString *const kMXKSlashCmdBanUser = @"/ban";
NSString *const kMXKSlashCmdUnbanUser = @"/unban";
NSString *const kMXKSlashCmdSetUserPowerLevel = @"/op";
NSString *const kMXKSlashCmdResetUserPowerLevel = @"/deop";
NSString *const kMXKSlashCmdChangeRoomTopic = @"/topic";
@@ -0,0 +1,40 @@
//
// Copyright 2020 The Matrix.org Foundation C.I.C
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
@protocol MXKURLPreviewDataProtocol <NSObject>
/// The URL that's represented by the preview data.
@property (readonly, nonnull) NSURL *url;
/// The ID of the event that created this preview.
@property (readonly, nonnull) NSString *eventID;
/// The ID of the room that this preview is from.
@property (readonly, nonnull) NSString *roomID;
/// The OpenGraph site name for the URL.
@property (readonly, nullable) NSString *siteName;
/// The OpenGraph title for the URL.
@property (readonly, nullable) NSString *title;
/// The OpenGraph description for the URL.
@property (readonly, nullable) NSString *text;
/// The OpenGraph image for the URL.
@property (readwrite, nullable) UIImage *image;
@end
@@ -0,0 +1,26 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKRecentsDataSource.h"
/**
'MXKInterleavedRecentsDataSource' class inherits from 'MXKRecentsDataSource'.
It interleaves the recents in case of multiple sessions to display first the most recent room.
*/
@interface MXKInterleavedRecentsDataSource : MXKRecentsDataSource
@end
@@ -0,0 +1,439 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKInterleavedRecentsDataSource.h"
#import "MXKInterleavedRecentTableViewCell.h"
#import "MXKAccountManager.h"
#import "NSBundle+MatrixKit.h"
@interface MXKInterleavedRecentsDataSource ()
{
/**
The interleaved recents: cell data served by `MXKInterleavedRecentsDataSource`.
*/
NSMutableArray *interleavedCellDataArray;
}
@end
@implementation MXKInterleavedRecentsDataSource
- (instancetype)init
{
self = [super init];
if (self)
{
interleavedCellDataArray = [NSMutableArray array];
}
return self;
}
#pragma mark - Override MXKDataSource
- (void)destroy
{
interleavedCellDataArray = nil;
[super destroy];
}
#pragma mark - Override MXKRecentsDataSource
- (UIView *)viewForHeaderInSection:(NSInteger)section withFrame:(CGRect)frame
{
UIView *sectionHeader = nil;
if (displayedRecentsDataSourceArray.count > 1 && section == 0)
{
sectionHeader = [[UIView alloc] initWithFrame:frame];
sectionHeader.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1.0];
CGFloat btnWidth = frame.size.width / displayedRecentsDataSourceArray.count;
UIButton *previousShrinkButton;
for (NSInteger index = 0; index < displayedRecentsDataSourceArray.count; index++)
{
MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:index];
NSString* btnTitle = recentsDataSource.mxSession.myUser.userId;
// Add shrink button
UIButton *shrinkButton = [UIButton buttonWithType:UIButtonTypeCustom];
CGRect btnFrame = CGRectMake(index * btnWidth, 0, btnWidth, sectionHeader.frame.size.height);
shrinkButton.frame = btnFrame;
shrinkButton.backgroundColor = [UIColor clearColor];
[shrinkButton addTarget:self action:@selector(onButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
shrinkButton.tag = index;
[sectionHeader addSubview:shrinkButton];
sectionHeader.userInteractionEnabled = YES;
// Set shrink button constraints
NSLayoutConstraint *leftConstraint;
NSLayoutConstraint *widthConstraint;
shrinkButton.translatesAutoresizingMaskIntoConstraints = NO;
if (!previousShrinkButton)
{
leftConstraint = [NSLayoutConstraint constraintWithItem:shrinkButton
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:sectionHeader
attribute:NSLayoutAttributeLeading
multiplier:1
constant:0];
widthConstraint = [NSLayoutConstraint constraintWithItem:shrinkButton
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:sectionHeader
attribute:NSLayoutAttributeWidth
multiplier:(1.0 /displayedRecentsDataSourceArray.count)
constant:0];
}
else
{
leftConstraint = [NSLayoutConstraint constraintWithItem:shrinkButton
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:previousShrinkButton
attribute:NSLayoutAttributeTrailing
multiplier:1
constant:0];
widthConstraint = [NSLayoutConstraint constraintWithItem:shrinkButton
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:previousShrinkButton
attribute:NSLayoutAttributeWidth
multiplier:1
constant:0];
}
[NSLayoutConstraint activateConstraints:@[leftConstraint, widthConstraint]];
previousShrinkButton = shrinkButton;
// Add shrink icon
UIImage *chevron;
if ([shrinkedRecentsDataSourceArray indexOfObject:recentsDataSource] != NSNotFound)
{
chevron = [NSBundle mxk_imageFromMXKAssetsBundleWithName:@"disclosure"];
}
else
{
chevron = [NSBundle mxk_imageFromMXKAssetsBundleWithName:@"shrink"];
}
UIImageView *chevronView = [[UIImageView alloc] initWithImage:chevron];
if ([shrinkedRecentsDataSourceArray indexOfObject:recentsDataSource] == NSNotFound)
{
// Display the tint color of the user
MXKAccount *account = [[MXKAccountManager sharedManager] accountForUserId:recentsDataSource.mxSession.myUser.userId];
if (account)
{
chevronView.backgroundColor = account.userTintColor;
}
else
{
chevronView.backgroundColor = [UIColor clearColor];
}
}
else
{
chevronView.backgroundColor = [UIColor lightGrayColor];
}
chevronView.contentMode = UIViewContentModeCenter;
frame = chevronView.frame;
frame.size.width = frame.size.height = shrinkButton.frame.size.height - 10;
frame.origin.x = shrinkButton.frame.size.width - frame.size.width - 8;
frame.origin.y = (shrinkButton.frame.size.height - frame.size.height) / 2;
chevronView.frame = frame;
[shrinkButton addSubview:chevronView];
chevronView.autoresizingMask |= (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin);
// Add label
frame = shrinkButton.frame;
frame.origin.x = 5;
frame.origin.y = 5;
frame.size.width = chevronView.frame.origin.x - 10;
frame.size.height -= 10;
UILabel *headerLabel = [[UILabel alloc] initWithFrame:frame];
headerLabel.font = [UIFont boldSystemFontOfSize:16];
headerLabel.backgroundColor = [UIColor clearColor];
headerLabel.text = btnTitle;
[shrinkButton addSubview:headerLabel];
headerLabel.autoresizingMask |= (UIViewAutoresizingFlexibleWidth);
}
}
return sectionHeader;
}
- (id<MXKRecentCellDataStoring>)cellDataAtIndexPath:(NSIndexPath *)indexPath
{
id<MXKRecentCellDataStoring> cellData = nil;
// Only one section is handled by this data source
if (indexPath.section == 0)
{
// Consider first the case where there is only one data source (no interleaving).
if (displayedRecentsDataSourceArray.count == 1)
{
MXKSessionRecentsDataSource *recentsDataSource = displayedRecentsDataSourceArray.firstObject;
cellData = [recentsDataSource cellDataAtIndex:indexPath.row];
}
// Else all the cells have been interleaved.
else if (indexPath.row < interleavedCellDataArray.count)
{
cellData = interleavedCellDataArray[indexPath.row];
}
}
return cellData;
}
- (CGFloat)cellHeightAtIndexPath:(NSIndexPath *)indexPath
{
CGFloat height = 0;
// Only one section is handled by this data source
if (indexPath.section == 0)
{
// Consider first the case where there is only one data source (no interleaving).
if (displayedRecentsDataSourceArray.count == 1)
{
MXKSessionRecentsDataSource *recentsDataSource = displayedRecentsDataSourceArray.firstObject;
height = [recentsDataSource cellHeightAtIndex:indexPath.row];
}
// Else all the cells have been interleaved.
else if (indexPath.row < interleavedCellDataArray.count)
{
id<MXKRecentCellDataStoring> recentCellData = interleavedCellDataArray[indexPath.row];
// Select the related recent data source
MXKDataSource *dataSource = recentCellData.dataSource;
if ([dataSource isKindOfClass:[MXKSessionRecentsDataSource class]])
{
MXKSessionRecentsDataSource *recentsDataSource = (MXKSessionRecentsDataSource*)dataSource;
// Count the index of this cell data in original data source array
NSInteger rank = 0;
for (NSInteger index = 0; index < indexPath.row; index++)
{
id<MXKRecentCellDataStoring> cellData = interleavedCellDataArray[index];
if (cellData.roomSummary == recentCellData.roomSummary)
{
rank++;
}
}
height = [recentsDataSource cellHeightAtIndex:rank];
}
}
}
return height;
}
- (NSIndexPath*)cellIndexPathWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)matrixSession
{
NSIndexPath *indexPath = nil;
// Consider first the case where there is only one data source (no interleaving).
if (displayedRecentsDataSourceArray.count == 1)
{
MXKSessionRecentsDataSource *recentsDataSource = displayedRecentsDataSourceArray.firstObject;
if (recentsDataSource.mxSession == matrixSession)
{
// Look for the cell
for (NSInteger index = 0; index < recentsDataSource.numberOfCells; index ++)
{
id<MXKRecentCellDataStoring> recentCellData = [recentsDataSource cellDataAtIndex:index];
if ([roomId isEqualToString:recentCellData.roomIdentifier])
{
// Got it
indexPath = [NSIndexPath indexPathForRow:index inSection:0];
break;
}
}
}
}
else
{
// Look for the right data source
for (MXKSessionRecentsDataSource *recentsDataSource in displayedRecentsDataSourceArray)
{
if (recentsDataSource.mxSession == matrixSession)
{
// Check whether the source is not shrinked
if ([shrinkedRecentsDataSourceArray indexOfObject:recentsDataSource] == NSNotFound)
{
// Look for the cell
for (NSInteger index = 0; index < interleavedCellDataArray.count; index ++)
{
id<MXKRecentCellDataStoring> recentCellData = interleavedCellDataArray[index];
if ([roomId isEqualToString:recentCellData.roomIdentifier])
{
// Got it
indexPath = [NSIndexPath indexPathForRow:index inSection:0];
break;
}
}
}
break;
}
}
}
return indexPath;
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Check whether all data sources are ready before rendering recents
if (self.state == MXKDataSourceStateReady)
{
// Only one section is handled by this data source.
return (displayedRecentsDataSourceArray.count ? 1 : 0);
}
return 0;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Consider first the case where there is only one data source (no interleaving).
if (displayedRecentsDataSourceArray.count == 1)
{
MXKSessionRecentsDataSource *recentsDataSource = displayedRecentsDataSourceArray.firstObject;
return recentsDataSource.numberOfCells;
}
return interleavedCellDataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
id<MXKRecentCellDataStoring> roomData = [self cellDataAtIndexPath:indexPath];
if (roomData && self.delegate)
{
NSString *cellIdentifier = [self.delegate cellReuseIdentifierForCellData:roomData];
if (cellIdentifier)
{
UITableViewCell<MXKCellRendering> *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
// Make sure we listen to user actions on the cell
cell.delegate = self;
// Make the bubble display the data
[cell render:roomData];
// Clear the user flag, if only one recents list is available
if (displayedRecentsDataSourceArray.count == 1 && [cell isKindOfClass:[MXKInterleavedRecentTableViewCell class]])
{
((MXKInterleavedRecentTableViewCell*)cell).userFlag.backgroundColor = [UIColor clearColor];
}
return cell;
}
}
// Return a fake cell to prevent app from crashing.
return [[UITableViewCell alloc] init];
}
#pragma mark - MXKDataSourceDelegate
- (void)dataSource:(MXKDataSource*)dataSource didCellChange:(id)changes
{
// Consider first the case where there is only one data source (no interleaving).
if (displayedRecentsDataSourceArray.count == 1)
{
// Flush interleaved cells array, we will refer directly to the cell data of the unique data source.
[interleavedCellDataArray removeAllObjects];
}
else
{
// Handle here the specific case where a second source is just added.
// The empty interleaved cells array has to be prefilled with the cell data of the other source (except if this other source is shrinked).
if (!interleavedCellDataArray.count && displayedRecentsDataSourceArray.count == 2)
{
// This is the first interleaving, look for the other source
MXKSessionRecentsDataSource *recentsDataSource = displayedRecentsDataSourceArray.firstObject;
if (recentsDataSource == dataSource)
{
recentsDataSource = displayedRecentsDataSourceArray.lastObject;
}
if ([shrinkedRecentsDataSourceArray indexOfObject:recentsDataSource] == NSNotFound)
{
// Report all cell data
for (NSInteger index = 0; index < recentsDataSource.numberOfCells; index ++)
{
[interleavedCellDataArray addObject:[recentsDataSource cellDataAtIndex:index]];
}
}
}
// Update now interleaved cells array, TODO take into account 'changes' parameter
MXKSessionRecentsDataSource *updateRecentsDataSource = (MXKSessionRecentsDataSource*)dataSource;
NSInteger numberOfUpdatedCells = 0;
// Check whether this dataSource is used
if ([displayedRecentsDataSourceArray indexOfObject:dataSource] != NSNotFound && [shrinkedRecentsDataSourceArray indexOfObject:dataSource] == NSNotFound)
{
numberOfUpdatedCells = updateRecentsDataSource.numberOfCells;
}
NSInteger currentCellIndex = 0;
NSInteger updatedCellIndex = 0;
id<MXKRecentCellDataStoring> updatedCellData = nil;
if (numberOfUpdatedCells)
{
updatedCellData = [updateRecentsDataSource cellDataAtIndex:updatedCellIndex++];
}
// Review all cell data items of the current list
while (currentCellIndex < interleavedCellDataArray.count)
{
id<MXKRecentCellDataStoring> currentCellData = interleavedCellDataArray[currentCellIndex];
// Remove existing cell data of the updated data source
if (currentCellData.dataSource == dataSource)
{
[interleavedCellDataArray removeObjectAtIndex:currentCellIndex];
}
else
{
while (updatedCellData && (updatedCellData.roomSummary.lastMessage.originServerTs > currentCellData.roomSummary.lastMessage.originServerTs))
{
[interleavedCellDataArray insertObject:updatedCellData atIndex:currentCellIndex++];
updatedCellData = [updateRecentsDataSource cellDataAtIndex:updatedCellIndex++];
}
currentCellIndex++;
}
}
while (updatedCellData)
{
[interleavedCellDataArray addObject:updatedCellData];
updatedCellData = [updateRecentsDataSource cellDataAtIndex:updatedCellIndex++];
}
}
// Call super to keep update readyRecentsDataSourceArray.
[super dataSource:dataSource didCellChange:changes];
}
@end
@@ -0,0 +1,26 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import "MXKRecentCellDataStoring.h"
/**
`MXKRecentCellData` modelised the data for a `MXKRecentTableViewCell` cell.
*/
@interface MXKRecentCellData : MXKCellData <MXKRecentCellDataStoring>
@end
@@ -0,0 +1,133 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKRecentCellData.h"
@import MatrixSDK;
#import "MXKDataSource.h"
#import "MXEvent+MatrixKit.h"
#import "MXKSwiftHeader.h"
@implementation MXKRecentCellData
@synthesize roomSummary, dataSource, lastEventDate;
- (instancetype)initWithRoomSummary:(id<MXRoomSummaryProtocol>)theRoomSummary
dataSource:(MXKDataSource*)theDataSource;
{
self = [self init];
if (self)
{
roomSummary = theRoomSummary;
dataSource = theDataSource;
}
return self;
}
- (void)dealloc
{
roomSummary = nil;
}
- (MXSession *)mxSession
{
return dataSource.mxSession;
}
- (NSString*)lastEventDate
{
return (NSString*)roomSummary.lastMessage.others[@"lastEventDate"];
}
- (BOOL)hasUnread
{
return (roomSummary.localUnreadEventCount != 0);
}
- (NSString *)roomIdentifier
{
if (self.isSuggestedRoom)
{
return self.roomSummary.spaceChildInfo.childRoomId;
}
return roomSummary.roomId;
}
- (NSString *)roomDisplayname
{
if (self.isSuggestedRoom)
{
return self.roomSummary.spaceChildInfo.displayName;
}
return roomSummary.displayname;
}
- (NSString *)avatarUrl
{
if (self.isSuggestedRoom)
{
return self.roomSummary.spaceChildInfo.avatarUrl;
}
return roomSummary.avatar;
}
- (NSString *)lastEventTextMessage
{
if (self.isSuggestedRoom)
{
return roomSummary.spaceChildInfo.topic;
}
return roomSummary.lastMessage.text;
}
- (NSAttributedString *)lastEventAttributedTextMessage
{
if (self.isSuggestedRoom)
{
return nil;
}
return roomSummary.lastMessage.attributedText;
}
- (NSUInteger)notificationCount
{
return roomSummary.notificationCount;
}
- (NSUInteger)highlightCount
{
return roomSummary.highlightCount;
}
- (NSString*)notificationCountStringValue
{
return [NSString stringWithFormat:@"%tu", self.notificationCount];
}
- (NSString*)description
{
return [NSString stringWithFormat:@"%@ %@: %@ - %@", super.description, self.roomSummary.roomId, self.roomDisplayname, self.lastEventTextMessage];
}
- (BOOL)isSuggestedRoom
{
// As off now, we only store MXSpaceChildInfo in case of suggested rooms
return self.roomSummary.spaceChildInfo != nil;
}
@end
@@ -0,0 +1,75 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <MatrixSDK/MatrixSDK.h>
#import "MXKCellData.h"
@class MXKDataSource;
@class MXSpaceChildInfo;
/**
`MXKRecentCellDataStoring` defines a protocol a class must conform in order to store recent cell data
managed by `MXKSessionRecentsDataSource`.
*/
@protocol MXKRecentCellDataStoring <NSObject>
#pragma mark - Data displayed by a room recent cell
/**
The original data source of the recent displayed by the cell.
*/
@property (nonatomic, weak, readonly) MXKDataSource *dataSource;
/**
The `MXRoomSummaryProtocol` instance of the room for the recent displayed by the cell.
*/
@property (nonatomic, readonly) id<MXRoomSummaryProtocol> roomSummary;
@property (nonatomic, readonly) NSString *roomIdentifier;
@property (nonatomic, readonly) NSString *roomDisplayname;
@property (nonatomic, readonly) NSString *avatarUrl;
@property (nonatomic, readonly) NSString *lastEventTextMessage;
@property (nonatomic, readonly) NSString *lastEventDate;
@property (nonatomic, readonly) BOOL hasUnread;
@property (nonatomic, readonly) NSUInteger notificationCount;
@property (nonatomic, readonly) NSUInteger highlightCount;
@property (nonatomic, readonly) NSString *notificationCountStringValue;
@property (nonatomic, readonly) BOOL isSuggestedRoom;
@property (nonatomic, readonly) MXSession *mxSession;
#pragma mark - Public methods
/**
Create a new `MXKCellData` object for a new recent cell.
@param roomSummary the `id<MXRoomSummaryProtocol>` object that has data about the room.
@param dataSource the `MXKDataSource` object that will use this instance.
@return the newly created instance.
*/
- (instancetype)initWithRoomSummary:(id<MXRoomSummaryProtocol>)roomSummary
dataSource:(MXKDataSource*)dataSource;
@optional
/**
The `lastEventTextMessage` with sets of attributes.
*/
@property (nonatomic, readonly) NSAttributedString *lastEventAttributedTextMessage;
@end
@@ -0,0 +1,140 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKSessionRecentsDataSource.h"
/**
'MXKRecentsDataSource' is a base class to handle recents from one or multiple matrix sessions.
A 'MXKRecentsDataSource' instance provides the recents data source for `MXKRecentListViewController`.
By default, the recents list of different sessions are handled into separate sections.
*/
@interface MXKRecentsDataSource : MXKDataSource <UITableViewDataSource, MXKDataSourceDelegate>
{
@protected
/**
Array of `MXKSessionRecentsDataSource` instances. Only ready and non empty data source are listed here.
(Note: a data source may be considered as empty during searching)
*/
NSMutableArray *displayedRecentsDataSourceArray;
/**
Array of shrinked sources. Sub-list of displayedRecentsDataSourceArray.
*/
NSMutableArray *shrinkedRecentsDataSourceArray;
}
/**
List of associated matrix sessions.
*/
@property (nonatomic, readonly) NSArray* mxSessions;
/**
The number of available recents data sources (This count may be different than mxSession.count because empty data sources are ignored).
*/
@property (nonatomic, readonly) NSUInteger displayedRecentsDataSourcesCount;
/**
Tell whether there are some unread messages.
*/
@property (nonatomic, readonly) BOOL hasUnread;
/**
The current search patterns list.
*/
@property (nonatomic, readonly) NSArray* searchPatternsList;
@property (nonatomic, strong) MXSpace *currentSpace;
#pragma mark - Configuration
/**
Add recents data from a matrix session.
@param mxSession the Matrix session to retrieve contextual data.
@return the new 'MXKSessionRecentsDataSource' instance created for this Matrix session.
*/
- (MXKSessionRecentsDataSource *)addMatrixSession:(MXSession*)mxSession;
/**
Remove recents data related to a matrix session.
@param mxSession the session to remove.
*/
- (void)removeMatrixSession:(MXSession*)mxSession;
/**
Filter the current recents list according to the provided patterns.
@param patternsList the list of patterns (`NSString` instances) to match with. Set nil to cancel search.
*/
- (void)searchWithPatterns:(NSArray*)patternsList;
/**
Get the section header view.
@param section the section index
@param frame the drawing area for the header of the specified section.
@return the section header.
*/
- (UIView *)viewForHeaderInSection:(NSInteger)section withFrame:(CGRect)frame;
/**
Get the data for the cell at the given index path.
@param indexPath the index of the cell
@return the cell data
*/
- (id<MXKRecentCellDataStoring>)cellDataAtIndexPath:(NSIndexPath*)indexPath;
/**
Get the height of the cell at the given index path.
@param indexPath the index of the cell
@return the cell height
*/
- (CGFloat)cellHeightAtIndexPath:(NSIndexPath*)indexPath;
/**
Get the index path of the cell related to the provided roomId and session.
@param roomId the room identifier.
@param mxSession the matrix session in which the room should be available.
@return indexPath the index of the cell (nil if not found or if the related section is shrinked).
*/
- (NSIndexPath*)cellIndexPathWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)mxSession;
/**
Returns the room at the index path
@param indexPath the index of the cell
@return the MXRoom if it exists
*/
- (MXRoom*)getRoomAtIndexPath:(NSIndexPath *)indexPath;
/**
Leave the room at the index path
@param indexPath the index of the cell
*/
- (void)leaveRoomAtIndexPath:(NSIndexPath *)indexPath;
/**
Action registered on buttons used to shrink/disclose recents sources.
*/
- (IBAction)onButtonPressed:(id)sender;
@end
@@ -0,0 +1,657 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKRecentsDataSource.h"
@import MatrixSDK.MXMediaManager;
#import "NSBundle+MatrixKit.h"
#import "MXKConstants.h"
@interface MXKRecentsDataSource ()
{
/**
Array of `MXSession` instances.
*/
NSMutableArray *mxSessionArray;
/**
Array of `MXKSessionRecentsDataSource` instances (one by matrix session).
*/
NSMutableArray *recentsDataSourceArray;
}
@end
@implementation MXKRecentsDataSource
- (instancetype)init
{
self = [super init];
if (self)
{
mxSessionArray = [NSMutableArray array];
recentsDataSourceArray = [NSMutableArray array];
displayedRecentsDataSourceArray = [NSMutableArray array];
shrinkedRecentsDataSourceArray = [NSMutableArray array];
// Set default data and view classes
[self registerCellDataClass:MXKRecentCellData.class forCellIdentifier:kMXKRecentCellIdentifier];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionInviteRoomUpdate:) name:kMXSessionInvitedRoomsDidChangeNotification object:nil];
}
return self;
}
- (instancetype)initWithMatrixSession:(MXSession *)matrixSession
{
self = [self init];
if (self)
{
[self addMatrixSession:matrixSession];
}
return self;
}
- (MXKSessionRecentsDataSource *)addMatrixSession:(MXSession *)matrixSession
{
MXKSessionRecentsDataSource *recentsDataSource = [[MXKSessionRecentsDataSource alloc] initWithMatrixSession:matrixSession];
if (recentsDataSource)
{
// Set the actual data and view classes
[recentsDataSource registerCellDataClass:[self cellDataClassForCellIdentifier:kMXKRecentCellIdentifier] forCellIdentifier:kMXKRecentCellIdentifier];
[mxSessionArray addObject:matrixSession];
recentsDataSource.delegate = self;
[recentsDataSourceArray addObject:recentsDataSource];
[recentsDataSource finalizeInitialization];
if (self.delegate && [self.delegate respondsToSelector:@selector(dataSource:didAddMatrixSession:)])
{
[self.delegate dataSource:self didAddMatrixSession:matrixSession];
}
// Check the current state of the data source
[self dataSource:recentsDataSource didStateChange:recentsDataSource.state];
}
return recentsDataSource;
}
- (void)removeMatrixSession:(MXSession*)matrixSession
{
for (NSUInteger index = 0; index < mxSessionArray.count; index++)
{
MXSession *mxSession = [mxSessionArray objectAtIndex:index];
if (mxSession == matrixSession)
{
MXKSessionRecentsDataSource *recentsDataSource = [recentsDataSourceArray objectAtIndex:index];
[recentsDataSource destroy];
[displayedRecentsDataSourceArray removeObject:recentsDataSource];
[recentsDataSourceArray removeObjectAtIndex:index];
[mxSessionArray removeObjectAtIndex:index];
// Loop on 'didCellChange' method to let inherited 'MXKRecentsDataSource' class handle this removed data source.
[self dataSource:recentsDataSource didCellChange:nil];
if (self.delegate && [self.delegate respondsToSelector:@selector(dataSource:didRemoveMatrixSession:)])
{
[self.delegate dataSource:self didRemoveMatrixSession:matrixSession];
}
break;
}
}
}
- (void)setCurrentSpace:(MXSpace *)currentSpace
{
_currentSpace = currentSpace;
for (MXKSessionRecentsDataSource *recentsDataSource in recentsDataSourceArray) {
recentsDataSource.currentSpace = currentSpace;
}
}
#pragma mark - MXKDataSource overridden
- (MXSession*)mxSession
{
if (mxSessionArray.count > 1)
{
MXLogDebug(@"[MXKRecentsDataSource] CAUTION: mxSession property is not relevant in case of multi-sessions (%tu)", mxSessionArray.count);
}
// TODO: This property is not well adapted in case of multi-sessions
// We consider by default the first added session as the main one...
if (mxSessionArray.count)
{
return [mxSessionArray firstObject];
}
return nil;
}
- (MXKDataSourceState)state
{
// Manage a global state based on the state of each internal data source.
MXKDataSourceState currentState = MXKDataSourceStateUnknown;
MXKSessionRecentsDataSource *dataSource;
if (recentsDataSourceArray.count)
{
dataSource = [recentsDataSourceArray firstObject];
currentState = dataSource.state;
// Deduce the current state according to the internal data sources
for (NSUInteger index = 1; index < recentsDataSourceArray.count; index++)
{
dataSource = [recentsDataSourceArray objectAtIndex:index];
switch (dataSource.state)
{
case MXKDataSourceStateUnknown:
break;
case MXKDataSourceStatePreparing:
currentState = MXKDataSourceStatePreparing;
break;
case MXKDataSourceStateFailed:
if (currentState == MXKDataSourceStateUnknown)
{
currentState = MXKDataSourceStateFailed;
}
break;
case MXKDataSourceStateReady:
if (currentState == MXKDataSourceStateUnknown || currentState == MXKDataSourceStateFailed)
{
currentState = MXKDataSourceStateReady;
}
break;
default:
break;
}
}
}
return currentState;
}
- (void)destroy
{
for (MXKSessionRecentsDataSource *recentsDataSource in recentsDataSourceArray)
{
[recentsDataSource destroy];
}
displayedRecentsDataSourceArray = nil;
recentsDataSourceArray = nil;
shrinkedRecentsDataSourceArray = nil;
mxSessionArray = nil;
_searchPatternsList = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionInvitedRoomsDidChangeNotification object:nil];
[super destroy];
}
#pragma mark -
- (NSArray*)mxSessions
{
return [NSArray arrayWithArray:mxSessionArray];
}
- (NSUInteger)displayedRecentsDataSourcesCount
{
return displayedRecentsDataSourceArray.count;
}
- (BOOL)hasUnread
{
// Check hasUnread flag in all ready data sources
for (MXKSessionRecentsDataSource *recentsDataSource in displayedRecentsDataSourceArray)
{
if (recentsDataSource.hasUnread)
{
return YES;
}
}
return NO;
}
- (void)searchWithPatterns:(NSArray*)patternsList
{
_searchPatternsList = patternsList;
// CAUTION: Apply here the search pattern to all ready data sources (not only displayed ones).
// Some data sources may have been removed from 'displayedRecentsDataSourceArray' during a previous search if no recent was matching.
for (MXKSessionRecentsDataSource *recentsDataSource in recentsDataSourceArray)
{
if (recentsDataSource.state == MXKDataSourceStateReady)
{
[recentsDataSource searchWithPatterns:patternsList];
}
}
}
- (UIView *)viewForHeaderInSection:(NSInteger)section withFrame:(CGRect)frame
{
UIView *sectionHeader = nil;
if (displayedRecentsDataSourceArray.count > 1 && section < displayedRecentsDataSourceArray.count)
{
MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:section];
NSString* sectionTitle = recentsDataSource.mxSession.myUser.userId;
sectionHeader = [[UIView alloc] initWithFrame:frame];
sectionHeader.backgroundColor = [UIColor colorWithRed:0.9 green:0.9 blue:0.9 alpha:1.0];
// Add shrink button
UIButton *shrinkButton = [UIButton buttonWithType:UIButtonTypeCustom];
frame.origin.x = frame.origin.y = 0;
shrinkButton.frame = frame;
shrinkButton.backgroundColor = [UIColor clearColor];
[shrinkButton addTarget:self action:@selector(onButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
shrinkButton.tag = section;
[sectionHeader addSubview:shrinkButton];
sectionHeader.userInteractionEnabled = YES;
// Add shrink icon
UIImage *chevron;
if ([shrinkedRecentsDataSourceArray indexOfObject:recentsDataSource] != NSNotFound)
{
chevron = [NSBundle mxk_imageFromMXKAssetsBundleWithName:@"disclosure"];
}
else
{
chevron = [NSBundle mxk_imageFromMXKAssetsBundleWithName:@"shrink"];
}
UIImageView *chevronView = [[UIImageView alloc] initWithImage:chevron];
chevronView.contentMode = UIViewContentModeCenter;
frame = chevronView.frame;
frame.origin.x = sectionHeader.frame.size.width - frame.size.width - 8;
frame.origin.y = (sectionHeader.frame.size.height - frame.size.height) / 2;
chevronView.frame = frame;
[sectionHeader addSubview:chevronView];
chevronView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin);
// Add label
frame = sectionHeader.frame;
frame.origin.x = 5;
frame.origin.y = 5;
frame.size.width = chevronView.frame.origin.x - 10;
frame.size.height -= 10;
UILabel *headerLabel = [[UILabel alloc] initWithFrame:frame];
headerLabel.font = [UIFont boldSystemFontOfSize:16];
headerLabel.backgroundColor = [UIColor clearColor];
headerLabel.text = sectionTitle;
[sectionHeader addSubview:headerLabel];
}
return sectionHeader;
}
- (id<MXKRecentCellDataStoring>)cellDataAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section < displayedRecentsDataSourceArray.count)
{
MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:indexPath.section];
return [recentsDataSource cellDataAtIndex:indexPath.row];
}
return nil;
}
- (CGFloat)cellHeightAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section < displayedRecentsDataSourceArray.count)
{
MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:indexPath.section];
return [recentsDataSource cellHeightAtIndex:indexPath.row];
}
return 0;
}
- (NSIndexPath*)cellIndexPathWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)matrixSession
{
NSIndexPath *indexPath = nil;
// Look for the right data source
for (NSInteger section = 0; section < displayedRecentsDataSourceArray.count; section++)
{
MXKSessionRecentsDataSource *recentsDataSource = displayedRecentsDataSourceArray[section];
if (recentsDataSource.mxSession == matrixSession)
{
// Check whether the source is not shrinked
if ([shrinkedRecentsDataSourceArray indexOfObject:recentsDataSource] == NSNotFound)
{
// Look for the cell
for (NSInteger index = 0; index < recentsDataSource.numberOfCells; index ++)
{
id<MXKRecentCellDataStoring> recentCellData = [recentsDataSource cellDataAtIndex:index];
if ([roomId isEqualToString:recentCellData.roomIdentifier])
{
// Got it
indexPath = [NSIndexPath indexPathForRow:index inSection:section];
break;
}
}
}
break;
}
}
return indexPath;
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Check whether all data sources are ready before rendering recents
if (self.state == MXKDataSourceStateReady)
{
return displayedRecentsDataSourceArray.count;
}
return 0;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (section < displayedRecentsDataSourceArray.count)
{
MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:section];
// Check whether the source is shrinked
if ([shrinkedRecentsDataSourceArray indexOfObject:recentsDataSource] == NSNotFound)
{
return recentsDataSource.numberOfCells;
}
}
return 0;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
NSString* sectionTitle = nil;
if (displayedRecentsDataSourceArray.count > 1 && section < displayedRecentsDataSourceArray.count)
{
MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:section];
sectionTitle = recentsDataSource.mxSession.myUser.userId;
}
return sectionTitle;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section < displayedRecentsDataSourceArray.count && self.delegate)
{
MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:indexPath.section];
id<MXKRecentCellDataStoring> roomData = [recentsDataSource cellDataAtIndex:indexPath.row];
NSString *cellIdentifier = [self.delegate cellReuseIdentifierForCellData:roomData];
if (cellIdentifier)
{
UITableViewCell<MXKCellRendering> *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
// Make sure we listen to user actions on the cell
cell.delegate = self;
// Make the bubble display the data
[cell render:roomData];
return cell;
}
}
// Return a fake cell to prevent app from crashing.
return [[UITableViewCell alloc] init];
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
// Return NO if you do not want the specified item to be editable.
return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
{
[self leaveRoomAtIndexPath:indexPath];
}
}
#pragma mark - MXKDataSourceDelegate
- (Class<MXKCellRendering>)cellViewClassForCellData:(MXKCellData*)cellData
{
// Retrieve the class from the delegate here
if (self.delegate)
{
return [self.delegate cellViewClassForCellData:cellData];
}
return nil;
}
- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData
{
// Retrieve the identifier from the delegate here
if (self.delegate)
{
return [self.delegate cellReuseIdentifierForCellData:cellData];
}
return nil;
}
- (void)dataSource:(MXKDataSource*)dataSource didCellChange:(id)changes
{
// Keep update readyRecentsDataSourceArray by checking number of cells
if (dataSource.state == MXKDataSourceStateReady)
{
MXKSessionRecentsDataSource *recentsDataSource = (MXKSessionRecentsDataSource*)dataSource;
if (recentsDataSource.numberOfCells)
{
// Check whether the data source must be added
if ([displayedRecentsDataSourceArray indexOfObject:recentsDataSource] == NSNotFound)
{
// Add this data source first
[self dataSource:dataSource didStateChange:dataSource.state];
return;
}
}
else
{
// Check whether this data source must be removed
if ([displayedRecentsDataSourceArray indexOfObject:recentsDataSource] != NSNotFound)
{
[displayedRecentsDataSourceArray removeObject:recentsDataSource];
// Loop on 'didCellChange' method to let inherited 'MXKRecentsDataSource' class handle this removed data source.
[self dataSource:recentsDataSource didCellChange:nil];
return;
}
}
}
// Notify delegate
[self.delegate dataSource:self didCellChange:changes];
}
- (void)dataSource:(MXKDataSource*)dataSource didStateChange:(MXKDataSourceState)state
{
// Update list of ready data sources
MXKSessionRecentsDataSource *recentsDataSource = (MXKSessionRecentsDataSource*)dataSource;
if (dataSource.state == MXKDataSourceStateReady && recentsDataSource.numberOfCells)
{
if ([displayedRecentsDataSourceArray indexOfObject:recentsDataSource] == NSNotFound)
{
// Add this new recents data source.
if (!displayedRecentsDataSourceArray.count)
{
[displayedRecentsDataSourceArray addObject:recentsDataSource];
}
else
{
// To display multiple accounts in a consistent order, we sort the recents data source by considering the account user id (alphabetic order).
NSUInteger index;
for (index = 0; index < displayedRecentsDataSourceArray.count; index++)
{
MXKSessionRecentsDataSource *currentRecentsDataSource = displayedRecentsDataSourceArray[index];
if ([currentRecentsDataSource.mxSession.myUser.userId compare:recentsDataSource.mxSession.myUser.userId] == NSOrderedDescending)
{
break;
}
}
// Insert this data source
[displayedRecentsDataSourceArray insertObject:recentsDataSource atIndex:index];
}
// Check whether a search session is in progress
if (_searchPatternsList)
{
[recentsDataSource searchWithPatterns:_searchPatternsList];
}
else
{
// Loop on 'didCellChange' method to let inherited 'MXKRecentsDataSource' class handle this new added data source.
[self dataSource:recentsDataSource didCellChange:nil];
}
}
}
else if ([displayedRecentsDataSourceArray indexOfObject:recentsDataSource] != NSNotFound)
{
[displayedRecentsDataSourceArray removeObject:recentsDataSource];
// Loop on 'didCellChange' method to let inherited 'MXKRecentsDataSource' class handle this removed data source.
[self dataSource:recentsDataSource didCellChange:nil];
}
if (self.delegate && [self.delegate respondsToSelector:@selector(dataSource:didStateChange:)])
{
[self.delegate dataSource:self didStateChange:self.state];
}
}
#pragma mark - Action
- (IBAction)onButtonPressed:(id)sender
{
if ([sender isKindOfClass:[UIButton class]])
{
UIButton *shrinkButton = (UIButton*)sender;
if (shrinkButton.tag < displayedRecentsDataSourceArray.count)
{
MXKSessionRecentsDataSource *recentsDataSource = [displayedRecentsDataSourceArray objectAtIndex:shrinkButton.tag];
NSUInteger index = [shrinkedRecentsDataSourceArray indexOfObject:recentsDataSource];
if (index != NSNotFound)
{
// Disclose the
[shrinkedRecentsDataSourceArray removeObjectAtIndex:index];
}
else
{
// Shrink the recents from this session
[shrinkedRecentsDataSourceArray addObject:recentsDataSource];
}
// Loop on 'didCellChange' method to let inherited 'MXKRecentsDataSource' class handle change on this data source.
[self dataSource:recentsDataSource didCellChange:nil];
}
}
}
#pragma mark - room actions
- (MXRoom*)getRoomAtIndexPath:(NSIndexPath *)indexPath
{
// Leave the selected room
id<MXKRecentCellDataStoring> recentCellData = [self cellDataAtIndexPath:indexPath];
if (recentCellData)
{
return [self.mxSession roomWithRoomId:recentCellData.roomIdentifier];
}
return nil;
}
- (void)leaveRoomAtIndexPath:(NSIndexPath *)indexPath
{
MXRoom* room = [self getRoomAtIndexPath:indexPath];
if (room)
{
// cancel pending uploads/downloads
// they are useless by now
[MXMediaManager cancelDownloadsInCacheFolder:room.roomId];
// TODO GFO cancel pending uploads related to this room
[room leave:^{
// Trigger recents table refresh
if (self.delegate)
{
[self.delegate dataSource:self didCellChange:nil];
}
} failure:^(NSError *error) {
MXLogDebug(@"[MXKRecentsDataSource] Failed to leave room (%@) failed", room.roomId);
// Notify MatrixKit user
NSString *myUserId = room.mxSession.myUser.userId;
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];
}];
}
}
- (void)didMXSessionInviteRoomUpdate:(NSNotification *)notif
{
MXSession *mxSession = notif.object;
if ([self.mxSessions indexOfObject:mxSession] != NSNotFound)
{
// do nothing by default
// the inherited classes might require to perform a full or a particial refresh.
//[self.delegate dataSource:self didCellChange:nil];
}
}
@end
@@ -0,0 +1,90 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <UIKit/UIKit.h>
#import "MXKConstants.h"
#import "MXKDataSource.h"
#import "MXKRecentCellData.h"
@class MXSpace;
/**
Identifier to use for cells that display a room in the recents list.
*/
extern NSString *const kMXKRecentCellIdentifier;
/**
The recents data source based on a unique matrix session.
*/
MXK_DEPRECATED_ATTRIBUTE_WITH_MSG("See MXSession.roomListDataManager")
@interface MXKSessionRecentsDataSource : MXKDataSource {
@protected
/**
The data for the cells served by `MXKSessionRecentsDataSource`.
*/
NSMutableArray *cellDataArray;
/**
The filtered recents: sub-list of `cellDataArray` defined by `searchWithPatterns:` call.
*/
NSMutableArray *filteredCellDataArray;
}
/**
The current number of cells.
*/
@property (nonatomic, readonly) NSInteger numberOfCells;
/**
Tell whether there are some unread messages.
*/
@property (nonatomic, readonly) BOOL hasUnread;
@property (nonatomic, strong, nullable) MXSpace *currentSpace;
#pragma mark - Life cycle
/**
Filter the current recents list according to the provided patterns.
When patterns are not empty, the search result is stored in `filteredCellDataArray`,
this array provides then data for the cells served by `MXKRecentsDataSource`.
@param patternsList the list of patterns (`NSString` instances) to match with. Set nil to cancel search.
*/
- (void)searchWithPatterns:(NSArray*)patternsList;
/**
Get the data for the cell at the given index.
@param index the index of the cell in the array
@return the cell data
*/
- (id<MXKRecentCellDataStoring>)cellDataAtIndex:(NSInteger)index;
/**
Get height of the cell at the given index.
@param index the index of the cell in the array
@return the cell height
*/
- (CGFloat)cellHeightAtIndex:(NSInteger)index;
@end
@@ -0,0 +1,552 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKSessionRecentsDataSource.h"
@import MatrixSDK;
#import "MXKRoomDataSourceManager.h"
#import "MXKSwiftHeader.h"
#pragma mark - Constant definitions
NSString *const kMXKRecentCellIdentifier = @"kMXKRecentCellIdentifier";
static NSTimeInterval const roomSummaryChangeThrottlerDelay = .5;
@interface MXKSessionRecentsDataSource ()
{
MXKRoomDataSourceManager *roomDataSourceManager;
/**
Internal array used to regulate change notifications.
Cell data changes are stored instantly in this array.
These changes are reported to the delegate only if no server sync is in progress.
*/
NSMutableArray *internalCellDataArray;
/**
Store the current search patterns list.
*/
NSArray* searchPatternsList;
/**
Do not react on every summary change
*/
MXThrottler *roomSummaryChangeThrottler;
/**
Last received suggested rooms per space ID
*/
NSMutableDictionary<NSString*, NSArray<MXSpaceChildInfo *> *> *lastSuggestedRooms;
/**
Event listener of the current space used to update the UI if an event occurs.
*/
id spaceEventsListener;
/**
Observer used to reload data when the space service is initialised
*/
id spaceServiceDidInitialiseObserver;
}
/**
Additional suggestedRooms related to the current selected Space
*/
@property (nonatomic, strong) NSArray<MXSpaceChildInfo *> *suggestedRooms;
@end
@implementation MXKSessionRecentsDataSource
- (instancetype)initWithMatrixSession:(MXSession *)matrixSession
{
self = [super initWithMatrixSession:matrixSession];
if (self)
{
roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mxSession];
internalCellDataArray = [NSMutableArray array];
filteredCellDataArray = nil;
lastSuggestedRooms = [NSMutableDictionary new];
// Set default data and view classes
[self registerCellDataClass:MXKRecentCellData.class forCellIdentifier:kMXKRecentCellIdentifier];
roomSummaryChangeThrottler = [[MXThrottler alloc] initWithMinimumDelay:roomSummaryChangeThrottlerDelay];
[[MXKAppSettings standardAppSettings] addObserver:self forKeyPath:@"showAllRoomsInHomeSpace" options:0 context:nil];
}
return self;
}
- (void)destroy
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXRoomSummaryDidChangeNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKRoomDataSourceSyncStatusChanged object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionNewRoomNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidLeaveRoomNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDirectRoomsDidChangeNotification object:nil];
if (spaceServiceDidInitialiseObserver) {
[[NSNotificationCenter defaultCenter] removeObserver:spaceServiceDidInitialiseObserver];
}
[roomSummaryChangeThrottler cancelAll];
roomSummaryChangeThrottler = nil;
cellDataArray = nil;
internalCellDataArray = nil;
filteredCellDataArray = nil;
lastSuggestedRooms = nil;
searchPatternsList = nil;
[[MXKAppSettings standardAppSettings] removeObserver:self forKeyPath:@"showAllRoomsInHomeSpace" context:nil];
[super destroy];
}
- (void)didMXSessionStateChange
{
if (MXSessionStateStoreDataReady <= self.mxSession.state)
{
// Check whether some data have been already load
if (0 == internalCellDataArray.count)
{
[self loadData];
}
else if (!roomDataSourceManager.isServerSyncInProgress)
{
// Sort cell data and notify the delegate
[self sortCellDataAndNotifyChanges];
}
}
}
- (void)setCurrentSpace:(MXSpace *)currentSpace
{
if (_currentSpace == currentSpace)
{
return;
}
if (_currentSpace && spaceEventsListener)
{
[_currentSpace.room removeListener:spaceEventsListener];
}
_currentSpace = currentSpace;
self.suggestedRooms = _currentSpace ? lastSuggestedRooms[_currentSpace.spaceId] : nil;
[self updateSuggestedRooms];
MXWeakify(self);
spaceEventsListener = [self.currentSpace.room listenToEvents:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) {
MXStrongifyAndReturnIfNil(self);
[self updateSuggestedRooms];
}];
}
-(void)setSuggestedRooms:(NSArray<MXSpaceChildInfo *> *)suggestedRooms
{
_suggestedRooms = suggestedRooms;
[self loadData];
}
-(void)updateSuggestedRooms
{
if (self.currentSpace)
{
NSString *currentSpaceId = self.currentSpace.spaceId;
MXWeakify(self);
[self.mxSession.spaceService getSpaceChildrenForSpaceWithId:currentSpaceId suggestedOnly:YES limit:5 maxDepth:1 paginationToken:nil success:^(MXSpaceChildrenSummary * _Nonnull childrenSummary) {
MXLogDebug(@"[MXKSessionRecentsDataSource] getSpaceChildrenForSpaceWithId %@: %ld found", self.currentSpace.spaceId, childrenSummary.childInfos.count);
MXStrongifyAndReturnIfNil(self);
self->lastSuggestedRooms[currentSpaceId] = childrenSummary.childInfos;
if ([self.currentSpace.spaceId isEqual:currentSpaceId]) {
self.suggestedRooms = childrenSummary.childInfos;
}
} failure:^(NSError * _Nonnull error) {
MXLogError(@"[MXKSessionRecentsDataSource] getSpaceChildrenForSpaceWithId failed with error: %@", error);
}];
}
}
#pragma mark -
- (NSInteger)numberOfCells
{
if (filteredCellDataArray)
{
return filteredCellDataArray.count;
}
return cellDataArray.count;
}
- (BOOL)hasUnread
{
// Check all current cells
// Use numberOfRowsInSection methods so that we take benefit of the filtering
for (NSUInteger i = 0; i < self.numberOfCells; i++)
{
id<MXKRecentCellDataStoring> cellData = [self cellDataAtIndex:i];
if (cellData.hasUnread)
{
return YES;
}
}
return NO;
}
- (void)searchWithPatterns:(NSArray*)patternsList
{
if (patternsList.count)
{
searchPatternsList = patternsList;
if (filteredCellDataArray)
{
[filteredCellDataArray removeAllObjects];
}
else
{
filteredCellDataArray = [NSMutableArray arrayWithCapacity:cellDataArray.count];
}
for (id<MXKRecentCellDataStoring> cellData in cellDataArray)
{
for (NSString* pattern in patternsList)
{
if (cellData.roomDisplayname && [cellData.roomDisplayname rangeOfString:pattern options:NSCaseInsensitiveSearch].location != NSNotFound)
{
[filteredCellDataArray addObject:cellData];
break;
}
}
}
}
else
{
filteredCellDataArray = nil;
searchPatternsList = nil;
}
[self.delegate dataSource:self didCellChange:nil];
}
- (id<MXKRecentCellDataStoring>)cellDataAtIndex:(NSInteger)index
{
if (filteredCellDataArray)
{
if (index < filteredCellDataArray.count)
{
return filteredCellDataArray[index];
}
}
else if (index < cellDataArray.count)
{
return cellDataArray[index];
}
return nil;
}
- (CGFloat)cellHeightAtIndex:(NSInteger)index
{
if (self.delegate)
{
id<MXKRecentCellDataStoring> cellData = [self cellDataAtIndex:index];
Class<MXKCellRendering> class = [self.delegate cellViewClassForCellData:cellData];
return [class heightForCellData:cellData withMaximumWidth:0];
}
return 0;
}
#pragma mark - Events processing
/**
Filtering in this method won't have any effect anymore. This class is not maintained.
*/
- (void)loadData
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXRoomSummaryDidChangeNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXKRoomDataSourceSyncStatusChanged object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionNewRoomNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDidLeaveRoomNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionDirectRoomsDidChangeNotification object:nil];
if (!self.mxSession.spaceService.isInitialised && !spaceServiceDidInitialiseObserver) {
MXWeakify(self);
spaceServiceDidInitialiseObserver = [[NSNotificationCenter defaultCenter] addObserverForName:MXSpaceService.didInitialise object:self.mxSession.spaceService queue:nil usingBlock:^(NSNotification * _Nonnull note) {
MXStrongifyAndReturnIfNil(self);
[self loadData];
}];
}
// Reset the table
[internalCellDataArray removeAllObjects];
// Retrieve the MXKCellData class to manage the data
Class class = [self cellDataClassForCellIdentifier:kMXKRecentCellIdentifier];
NSAssert([class conformsToProtocol:@protocol(MXKRecentCellDataStoring)], @"MXKSessionRecentsDataSource only manages MXKCellData that conforms to MXKRecentCellDataStoring protocol");
NSDate *startDate = [NSDate date];
for (MXRoomSummary *roomSummary in self.mxSession.roomsSummaries)
{
// Filter out private rooms with conference users
if (!roomSummary.isConferenceUserRoom // @TODO Abstract this condition with roomSummary.hiddenFromUser
&& !roomSummary.hiddenFromUser)
{
id<MXKRecentCellDataStoring> cellData = [[class alloc] initWithRoomSummary:roomSummary dataSource:self];
if (cellData)
{
[internalCellDataArray addObject:cellData];
}
}
}
for (MXSpaceChildInfo *childInfo in _suggestedRooms)
{
id<MXRoomSummaryProtocol> summary = [[MXRoomSummary alloc] initWithSpaceChildInfo:childInfo];
id<MXKRecentCellDataStoring> cellData = [[class alloc] initWithRoomSummary:summary
dataSource:self];
if (cellData)
{
[internalCellDataArray addObject:cellData];
}
}
MXLogDebug(@"[MXKSessionRecentsDataSource] Loaded %tu recents in %.3fms", self.mxSession.rooms.count, [[NSDate date] timeIntervalSinceDate:startDate] * 1000);
// Make sure all rooms have a last message
[self.mxSession fixRoomsSummariesLastMessage];
// Report loaded array except if sync is in progress
if (!roomDataSourceManager.isServerSyncInProgress)
{
[self sortCellDataAndNotifyChanges];
}
// Listen to MXSession rooms count changes
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionHaveNewRoom:) name:kMXSessionNewRoomNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionDidLeaveRoom:) name:kMXSessionDidLeaveRoomNotification object:nil];
// Listen to the direct rooms list
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didDirectRoomsChange:) name:kMXSessionDirectRoomsDidChangeNotification object:nil];
// Listen to MXRoomSummary
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didRoomSummaryChanged:) name:kMXRoomSummaryDidChangeNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXSessionStateChange) name:kMXKRoomDataSourceSyncStatusChanged object:nil];
}
- (void)didDirectRoomsChange:(NSNotification *)notif
{
// Inform the delegate about the update
[self.delegate dataSource:self didCellChange:nil];
}
- (void)didRoomSummaryChanged:(NSNotification *)notif
{
[roomSummaryChangeThrottler throttle:^{
[self didRoomSummaryChanged2:notif];
}];
}
- (void)didRoomSummaryChanged2:(NSNotification *)notif
{
MXRoomSummary *roomSummary = notif.object;
if (roomSummary.mxSession == self.mxSession && internalCellDataArray.count)
{
// Find the index of the related cell data
NSInteger index = NSNotFound;
for (index = 0; index < internalCellDataArray.count; index++)
{
id<MXKRecentCellDataStoring> theRoomData = [internalCellDataArray objectAtIndex:index];
if (theRoomData.roomSummary == roomSummary)
{
break;
}
}
if (index < internalCellDataArray.count)
{
if (roomSummary.hiddenFromUser)
{
[internalCellDataArray removeObjectAtIndex:index];
}
else
{
// Create a new instance to not modify the content of 'cellDataArray' (the copy is not a deep copy).
Class class = [self cellDataClassForCellIdentifier:kMXKRecentCellIdentifier];
id<MXKRecentCellDataStoring> cellData = [[class alloc] initWithRoomSummary:roomSummary dataSource:self];
if (cellData)
{
[internalCellDataArray replaceObjectAtIndex:index withObject:cellData];
}
}
// Report change except if sync is in progress
if (!roomDataSourceManager.isServerSyncInProgress)
{
[self sortCellDataAndNotifyChanges];
}
}
else
{
MXLogDebug(@"[MXKSessionRecentsDataSource] didRoomLastMessageChanged: Cannot find the changed room summary for %@ (%@). It is probably not managed by this recents data source", roomSummary.roomId, roomSummary);
}
}
else
{
// Inform the delegate that all the room summaries have been updated.
[self.delegate dataSource:self didCellChange:nil];
}
}
- (void)didMXSessionHaveNewRoom:(NSNotification *)notif
{
MXSession *mxSession = notif.object;
if (mxSession == self.mxSession)
{
NSString *roomId = notif.userInfo[kMXSessionNotificationRoomIdKey];
// Add the room if there is not yet a cell for it
id<MXKRecentCellDataStoring> roomData = [self cellDataWithRoomId:roomId];
if (nil == roomData)
{
MXLogDebug(@"MXKSessionRecentsDataSource] Add newly joined room: %@", roomId);
// Retrieve the MXKCellData class to manage the data
Class class = [self cellDataClassForCellIdentifier:kMXKRecentCellIdentifier];
MXRoomSummary *roomSummary = [mxSession roomSummaryWithRoomId:roomId];
id<MXKRecentCellDataStoring> cellData = [[class alloc] initWithRoomSummary:roomSummary dataSource:self];
if (cellData)
{
[internalCellDataArray addObject:cellData];
// Report change except if sync is in progress
if (!roomDataSourceManager.isServerSyncInProgress)
{
[self sortCellDataAndNotifyChanges];
}
}
}
}
}
- (void)didMXSessionDidLeaveRoom:(NSNotification *)notif
{
MXSession *mxSession = notif.object;
if (mxSession == self.mxSession)
{
NSString *roomId = notif.userInfo[kMXSessionNotificationRoomIdKey];
id<MXKRecentCellDataStoring> roomData = [self cellDataWithRoomId:roomId];
if (roomData)
{
MXLogDebug(@"MXKSessionRecentsDataSource] Remove left room: %@", roomId);
[internalCellDataArray removeObject:roomData];
// Report change except if sync is in progress
if (!roomDataSourceManager.isServerSyncInProgress)
{
[self sortCellDataAndNotifyChanges];
}
}
}
}
// Order cells
- (void)sortCellDataAndNotifyChanges
{
// Order them by origin_server_ts
[internalCellDataArray sortUsingComparator:^NSComparisonResult(id<MXKRecentCellDataStoring> cellData1, id<MXKRecentCellDataStoring> cellData2)
{
return [cellData1.roomSummary.lastMessage compareOriginServerTs:cellData2.roomSummary.lastMessage];
}];
// Snapshot the cell data array
cellDataArray = [internalCellDataArray copy];
// Update search result if any
if (searchPatternsList)
{
[self searchWithPatterns:searchPatternsList];
}
// Update here data source state
if (state != MXKDataSourceStateReady)
{
state = MXKDataSourceStateReady;
if (self.delegate && [self.delegate respondsToSelector:@selector(dataSource:didStateChange:)])
{
[self.delegate dataSource:self didStateChange:state];
}
}
// And inform the delegate about the update
[self.delegate dataSource:self didCellChange:nil];
}
// Find the cell data that stores information about the given room id
- (id<MXKRecentCellDataStoring>)cellDataWithRoomId:(NSString*)roomId
{
id<MXKRecentCellDataStoring> theRoomData;
NSMutableArray *dataArray = internalCellDataArray;
if (!roomDataSourceManager.isServerSyncInProgress)
{
dataArray = cellDataArray;
}
for (id<MXKRecentCellDataStoring> roomData in dataArray)
{
if ([roomData.roomSummary.roomId isEqualToString:roomId])
{
theRoomData = roomData;
break;
}
}
return theRoomData;
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if (object == [MXKAppSettings standardAppSettings] && [keyPath isEqualToString:@"showAllRoomsInHomeSpace"])
{
if (self.currentSpace == nil)
{
[self loadData];
}
}
}
@end
@@ -0,0 +1,33 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <MatrixSDK/MatrixSDK.h>
#import "MXKRoomMemberCellDataStoring.h"
/**
`MXKRoomMemberCellData` modelised the data for a `MXKRoomMemberTableViewCell` cell.
*/
@interface MXKRoomMemberCellData : MXKCellData <MXKRoomMemberCellDataStoring>
/**
The matrix session
*/
@property (nonatomic, readonly) MXSession *mxSession;
@end
@@ -0,0 +1,66 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKRoomMemberCellData.h"
#import "MXKRoomMemberListDataSource.h"
@interface MXKRoomMemberCellData ()
{
MXKRoomMemberListDataSource *roomMemberListDataSource;
}
@end
@implementation MXKRoomMemberCellData
@synthesize roomMember;
@synthesize memberDisplayName, powerLevel, isTyping;
- (instancetype)initWithRoomMember:(MXRoomMember*)member roomState:(MXRoomState*)roomState andRoomMemberListDataSource:(MXKRoomMemberListDataSource*)memberListDataSource
{
self = [self init];
if (self)
{
roomMember = member;
roomMemberListDataSource = memberListDataSource;
// Report member info from the current room state
memberDisplayName = [roomState.members memberName:roomMember.userId];
powerLevel = [roomState memberNormalizedPowerLevel:roomMember.userId];
isTyping = NO;
}
return self;
}
- (void)updateWithRoomState:(MXRoomState*)roomState
{
memberDisplayName = [roomState.members memberName:roomMember.userId];
powerLevel = [roomState memberNormalizedPowerLevel:roomMember.userId];
}
- (void)dealloc
{
roomMember = nil;
roomMemberListDataSource = nil;
}
- (MXSession*)mxSession
{
return roomMemberListDataSource.mxSession;
}
@end
@@ -0,0 +1,67 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <MatrixSDK/MatrixSDK.h>
#import "MXKCellData.h"
@class MXKRoomMemberListDataSource;
/**
`MXKRoomMemberCellDataStoring` defines a protocol a class must conform in order to store room member cell data
managed by `MXKRoomMemberListDataSource`.
*/
@protocol MXKRoomMemberCellDataStoring <NSObject>
#pragma mark - Data displayed by a room member cell
/**
The member displayed by the cell.
*/
@property (nonatomic, readonly) MXRoomMember *roomMember;
/**
The member display name
*/
@property (nonatomic, readonly) NSString *memberDisplayName;
/**
The member power level
*/
@property (nonatomic, readonly) CGFloat powerLevel;
/**
YES when member is typing in the room
*/
@property (nonatomic) BOOL isTyping;
#pragma mark - Public methods
/**
Create a new `MXKCellData` object for a new member cell.
@param memberListDataSource the `MXKRoomMemberListDataSource` object that will use this instance.
@return the newly created instance.
*/
- (instancetype)initWithRoomMember:(MXRoomMember*)member roomState:(MXRoomState*)roomState andRoomMemberListDataSource:(MXKRoomMemberListDataSource*)memberListDataSource;
/**
Update the member data with the provided roon state.
*/
- (void)updateWithRoomState:(MXRoomState*)roomState;
@end
@@ -0,0 +1,97 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <UIKit/UIKit.h>
#import <MatrixSDK/MatrixSDK.h>
#import "MXKDataSource.h"
#import "MXKRoomMemberCellData.h"
#import "MXKAppSettings.h"
/**
Identifier to use for cells that display a room member.
*/
extern NSString *const kMXKRoomMemberCellIdentifier;
/**
The data source for `MXKRoomMemberListViewController`.
*/
@interface MXKRoomMemberListDataSource : MXKDataSource <UITableViewDataSource> {
@protected
/**
The data for the cells served by `MXKRoomMembersDataSource`.
*/
NSMutableArray *cellDataArray;
/**
The filtered members: sub-list of `cellDataArray` defined by `searchWithPatterns:`.
*/
NSMutableArray *filteredCellDataArray;
}
/**
The id of the room managed by the data source.
*/
@property (nonatomic, readonly) NSString *roomId;
/**
The settings used to sort/display room members.
By default the shared application settings are considered.
*/
@property (nonatomic) MXKAppSettings *settings;
#pragma mark - Life cycle
/**
Initialise the data source to serve members corresponding to the passed room.
@param roomId the id of the room to get members from.
@param mxSession the Matrix session to get data from.
@return the newly created instance.
*/
- (instancetype)initWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)mxSession;
/**
Filter the current members list according to the provided patterns.
When patterns are not empty, the search result is stored in `filteredCellDataArray`,
this array provides then data for the cells served by `MXKRoomMembersDataSource`.
@param patternsList the list of patterns (`NSString` instances) to match with. Set nil to cancel search.
*/
- (void)searchWithPatterns:(NSArray*)patternsList;
/**
Get the data for the cell at the given index.
@param index the index of the cell in the array
@return the cell data
*/
- (id<MXKRoomMemberCellDataStoring>)cellDataAtIndex:(NSInteger)index;
/**
Get height of the celle at the given index.
@param index the index of the cell in the array
@return the cell height
*/
- (CGFloat)cellHeightAtIndex:(NSInteger)index;
@end
@@ -0,0 +1,464 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKRoomMemberListDataSource.h"
@import MatrixSDK.MXCallManager;
#import "MXKRoomMemberCellData.h"
#pragma mark - Constant definitions
NSString *const kMXKRoomMemberCellIdentifier = @"kMXKRoomMemberCellIdentifier";
@interface MXKRoomMemberListDataSource ()
{
/**
The room in which members are listed.
*/
MXRoom *mxRoom;
/**
Cache for loaded room state.
*/
MXRoomState *mxRoomState;
/**
The members events listener.
*/
id membersListener;
/**
The typing notification listener in the room.
*/
id typingNotifListener;
}
@end
@implementation MXKRoomMemberListDataSource
- (instancetype)initWithRoomId:(NSString*)roomId andMatrixSession:(MXSession*)mxSession
{
self = [super initWithMatrixSession:mxSession];
if (self)
{
_roomId = roomId;
cellDataArray = [NSMutableArray array];
filteredCellDataArray = nil;
// Consider the shared app settings by default
_settings = [MXKAppSettings standardAppSettings];
// Set default data class
[self registerCellDataClass:MXKRoomMemberCellData.class forCellIdentifier:kMXKRoomMemberCellIdentifier];
}
return self;
}
- (void)destroy
{
cellDataArray = nil;
filteredCellDataArray = nil;
if (membersListener)
{
[self.mxSession removeListener:membersListener];
membersListener = nil;
}
if (typingNotifListener)
{
MXWeakify(self);
[mxRoom liveTimeline:^(MXEventTimeline *liveTimeline) {
MXStrongifyAndReturnIfNil(self);
[liveTimeline removeListener:self->typingNotifListener];
self->typingNotifListener = nil;
}];
}
[super destroy];
}
- (void)didMXSessionStateChange
{
if (MXSessionStateStoreDataReady <= self.mxSession.state)
{
// Check whether the room is not already set
if (!mxRoom)
{
mxRoom = [self.mxSession roomWithRoomId:_roomId];
if (mxRoom)
{
MXWeakify(self);
[mxRoom state:^(MXRoomState *roomState) {
MXStrongifyAndReturnIfNil(self);
self->mxRoomState = roomState;
[self loadData];
// Register on typing notif
[self listenTypingNotifications];
// Register on members events
[self listenMembersEvents];
// Update here data source state
self->state = MXKDataSourceStateReady;
// Notify delegate
if (self.delegate)
{
if ([self.delegate respondsToSelector:@selector(dataSource:didStateChange:)])
{
[self.delegate dataSource:self didStateChange:self->state];
}
[self.delegate dataSource:self didCellChange:nil];
}
}];
}
else
{
MXLogDebug(@"[MXKRoomMemberDataSource] The user does not know the room %@", _roomId);
// Update here data source state
state = MXKDataSourceStateFailed;
// Notify delegate
if (self.delegate && [self.delegate respondsToSelector:@selector(dataSource:didStateChange:)])
{
[self.delegate dataSource:self didStateChange:state];
}
}
}
}
}
- (void)searchWithPatterns:(NSArray*)patternsList
{
if (patternsList.count)
{
if (filteredCellDataArray)
{
[filteredCellDataArray removeAllObjects];
}
else
{
filteredCellDataArray = [NSMutableArray arrayWithCapacity:cellDataArray.count];
}
for (id<MXKRoomMemberCellDataStoring> cellData in cellDataArray)
{
for (NSString* pattern in patternsList)
{
if ([[cellData memberDisplayName] rangeOfString:pattern options:NSCaseInsensitiveSearch].location != NSNotFound)
{
[filteredCellDataArray addObject:cellData];
break;
}
}
}
}
else
{
filteredCellDataArray = nil;
}
if (self.delegate)
{
[self.delegate dataSource:self didCellChange:nil];
}
}
- (id<MXKRoomMemberCellDataStoring>)cellDataAtIndex:(NSInteger)index
{
if (filteredCellDataArray)
{
return filteredCellDataArray[index];
}
return cellDataArray[index];
}
- (CGFloat)cellHeightAtIndex:(NSInteger)index
{
if (self.delegate)
{
id<MXKRoomMemberCellDataStoring> cellData = [self cellDataAtIndex:index];
Class<MXKCellRendering> class = [self.delegate cellViewClassForCellData:cellData];
return [class heightForCellData:cellData withMaximumWidth:0];
}
return 0;
}
#pragma mark - Members processing
- (void)loadData
{
NSArray* membersList = [mxRoomState.members membersWithoutConferenceUser];
if (!_settings.showLeftMembersInRoomMemberList)
{
NSMutableArray* filteredMembers = [[NSMutableArray alloc] init];
for (MXRoomMember* member in membersList)
{
// Filter out left users
if (member.membership != MXMembershipLeave)
{
[filteredMembers addObject:member];
}
}
membersList = filteredMembers;
}
[cellDataArray removeAllObjects];
// Retrieve the MXKCellData class to manage the data
Class class = [self cellDataClassForCellIdentifier:kMXKRoomMemberCellIdentifier];
NSAssert([class conformsToProtocol:@protocol(MXKRoomMemberCellDataStoring)], @"MXKRoomMemberListDataSource only manages MXKCellData that conforms to MXKRoomMemberCellDataStoring protocol");
for (MXRoomMember *member in membersList)
{
id<MXKRoomMemberCellDataStoring> cellData = [[class alloc] initWithRoomMember:member roomState:mxRoomState andRoomMemberListDataSource:self];
if (cellData)
{
[cellDataArray addObject:cellData];
}
}
[self sortMembers];
}
- (void)sortMembers
{
NSArray *sortedMembers = [cellDataArray sortedArrayUsingComparator:^NSComparisonResult(id<MXKRoomMemberCellDataStoring> member1, id<MXKRoomMemberCellDataStoring> member2)
{
// Move banned and left members at the end of the list
if (member1.roomMember.membership == MXMembershipLeave || member1.roomMember.membership == MXMembershipBan)
{
if (member2.roomMember.membership != MXMembershipLeave && member2.roomMember.membership != MXMembershipBan)
{
return NSOrderedDescending;
}
}
else if (member2.roomMember.membership == MXMembershipLeave || member2.roomMember.membership == MXMembershipBan)
{
return NSOrderedAscending;
}
// Move invited members just before left and banned members
if (member1.roomMember.membership == MXMembershipInvite)
{
if (member2.roomMember.membership != MXMembershipInvite)
{
return NSOrderedDescending;
}
}
else if (member2.roomMember.membership == MXMembershipInvite)
{
return NSOrderedAscending;
}
if (self->_settings.sortRoomMembersUsingLastSeenTime)
{
// Get the users that correspond to these members
MXUser *user1 = [self.mxSession userWithUserId:member1.roomMember.userId];
MXUser *user2 = [self.mxSession userWithUserId:member2.roomMember.userId];
// Move users who are not online or unavailable at the end (before invited users)
if ((user1.presence == MXPresenceOnline) || (user1.presence == MXPresenceUnavailable))
{
if ((user2.presence != MXPresenceOnline) && (user2.presence != MXPresenceUnavailable))
{
return NSOrderedAscending;
}
}
else if ((user2.presence == MXPresenceOnline) || (user2.presence == MXPresenceUnavailable))
{
return NSOrderedDescending;
}
else
{
// Here both users are neither online nor unavailable (the lastActive ago is useless)
// We will sort them according to their display, by keeping in front the offline users
if (user1.presence == MXPresenceOffline)
{
if (user2.presence != MXPresenceOffline)
{
return NSOrderedAscending;
}
}
else if (user2.presence == MXPresenceOffline)
{
return NSOrderedDescending;
}
return [[self->mxRoomState.members memberSortedName:member1.roomMember.userId] compare:[self->mxRoomState.members memberSortedName:member2.roomMember.userId] options:NSCaseInsensitiveSearch];
}
// Consider user's lastActive ago value
if (user1.lastActiveAgo < user2.lastActiveAgo)
{
return NSOrderedAscending;
}
else if (user1.lastActiveAgo == user2.lastActiveAgo)
{
return [[self->mxRoomState.members memberSortedName:member1.roomMember.userId] compare:[self->mxRoomState.members memberSortedName:member2.roomMember.userId] options:NSCaseInsensitiveSearch];
}
return NSOrderedDescending;
}
else
{
// Move user without display name at the end (before invited users)
if (member1.roomMember.displayname.length)
{
if (!member2.roomMember.displayname.length)
{
return NSOrderedAscending;
}
}
else if (member2.roomMember.displayname.length)
{
return NSOrderedDescending;
}
return [[self->mxRoomState.members memberSortedName:member1.roomMember.userId] compare:[self->mxRoomState.members memberSortedName:member2.roomMember.userId] options:NSCaseInsensitiveSearch];
}
}];
cellDataArray = [NSMutableArray arrayWithArray:sortedMembers];
}
- (void)listenMembersEvents
{
// Remove the previous live listener
if (membersListener)
{
[self.mxSession removeListener:membersListener];
}
// Register a listener for events that concern room members
NSArray *mxMembersEvents = @[
kMXEventTypeStringRoomMember,
kMXEventTypeStringRoomPowerLevels,
kMXEventTypeStringPresence
];
membersListener = [self.mxSession listenToEventsOfTypes:mxMembersEvents onEvent:^(MXEvent *event, MXTimelineDirection direction, id customObject)
{
// consider only live event
if (direction == MXTimelineDirectionForwards)
{
// Check the room Id (if any)
if (event.roomId && [event.roomId isEqualToString:self->mxRoom.roomId] == NO)
{
// This event does not concern the current room members
return;
}
// refresh the whole members list. TODO GFO refresh only the updated members.
[self loadData];
if (self.delegate)
{
[self.delegate dataSource:self didCellChange:nil];
}
}
}];
}
- (void)listenTypingNotifications
{
// Remove the previous live listener
if (self->typingNotifListener)
{
[mxRoom removeListener:self->typingNotifListener];
}
// Add typing notification listener
self->typingNotifListener = [mxRoom listenToEventsOfTypes:@[kMXEventTypeStringTypingNotification] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) {
// Handle only live events
if (direction == MXTimelineDirectionForwards)
{
// Retrieve typing users list
NSMutableArray *typingUsers = [NSMutableArray arrayWithArray:self->mxRoom.typingUsers];
// Remove typing info for the current user
NSUInteger index = [typingUsers indexOfObject:self.mxSession.myUser.userId];
if (index != NSNotFound)
{
[typingUsers removeObjectAtIndex:index];
}
for (id<MXKRoomMemberCellDataStoring> cellData in self->cellDataArray)
{
if ([typingUsers indexOfObject:cellData.roomMember.userId] == NSNotFound)
{
cellData.isTyping = NO;
}
else
{
cellData.isTyping = YES;
}
}
if (self.delegate)
{
[self.delegate dataSource:self didCellChange:nil];
}
}
}];
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (filteredCellDataArray)
{
return filteredCellDataArray.count;
}
return cellDataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
id<MXKRoomMemberCellDataStoring> roomData = [self cellDataAtIndex:indexPath.row];
if (roomData && self.delegate)
{
NSString *identifier = [self.delegate cellReuseIdentifierForCellData:roomData];
if (identifier)
{
UITableViewCell<MXKCellRendering> *cell = [tableView dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath];
// Make the bubble display the data
[cell render:roomData];
return cell;
}
}
// Return a fake cell to prevent app from crashing.
return [[UITableViewCell alloc] init];
}
@end
@@ -0,0 +1,25 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKCellData.h"
#import "MXKSearchCellDataStoring.h"
/**
`MXKSearchCellData` modelised the data for a `MXKSearchCell` cell.
*/
@interface MXKSearchCellData : MXKCellData <MXKSearchCellDataStoring>
@end
@@ -0,0 +1,69 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKSearchCellData.h"
#import "MXKSearchDataSource.h"
@implementation MXKSearchCellData
@synthesize roomId, senderDisplayName;
@synthesize searchResult, title, message, date, shouldShowRoomDisplayName, roomDisplayName, attachment, isAttachmentWithThumbnail, attachmentIcon;
- (instancetype)initWithSearchResult:(MXSearchResult *)searchResult2 andSearchDataSource:(MXKSearchDataSource *)searchDataSource
{
self = [super init];
if (self)
{
searchResult = searchResult2;
if (searchDataSource.roomEventFilter.rooms.count == 1)
{
// We are displaying a search within a room
// As title, display the user id
title = searchResult.result.sender;
roomId = searchDataSource.roomEventFilter.rooms[0];
}
else
{
// We are displaying a search over all user's rooms
// As title, display the room name of this search result
MXRoom *room = [searchDataSource.mxSession roomWithRoomId:searchResult.result.roomId];
if (room)
{
title = room.summary.displayname;
}
else
{
title = searchResult.result.roomId;
}
}
date = [searchDataSource.eventFormatter dateStringFromEvent:searchResult.result withTime:YES];
// Code from [MXEventFormatter stringFromEvent] for the particular case of a text message
message = [searchResult.result.content[@"body"] isKindOfClass:[NSString class]] ? searchResult.result.content[@"body"] : nil;
}
return self;
}
+ (void)cellDataWithSearchResult:(MXSearchResult *)searchResult andSearchDataSource:(MXKSearchDataSource *)searchDataSource onComplete:(void (^)(id<MXKSearchCellDataStoring>))onComplete
{
onComplete([[self alloc] initWithSearchResult:searchResult andSearchDataSource:searchDataSource]);
}
@end
@@ -0,0 +1,83 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <MatrixSDK/MatrixSDK.h>
#import "MXKAttachment.h"
@class MXKSearchDataSource;
/**
`MXKSearchCellDataStoring` defines a protocol a class must conform in order to store
a search result in a cell data managed by `MXKSearchDataSource`.
*/
@protocol MXKSearchCellDataStoring <NSObject>
/**
The room id
*/
@property (nonatomic) NSString *roomId;
@property (nonatomic, readonly) NSString *title;
@property (nonatomic, readonly) NSString *message;
@property (nonatomic, readonly) NSString *date;
// Bulk result returned by MatrixSDK
@property (nonatomic, readonly) MXSearchResult *searchResult;
/**
Tell whether the room display name should be displayed in the cell. NO by default.
*/
@property (nonatomic) BOOL shouldShowRoomDisplayName;
/**
The room display name.
*/
@property (nonatomic) NSString *roomDisplayName;
/**
The sender display name.
*/
@property (nonatomic) NSString *senderDisplayName;
/**
The bubble attachment (if any).
*/
@property (nonatomic) MXKAttachment *attachment;
/**
YES when the bubble correspond to an attachment displayed with a thumbnail (see image, video).
*/
@property (nonatomic, readonly) BOOL isAttachmentWithThumbnail;
/**
The default icon relative to the attachment (if any).
*/
@property (nonatomic, readonly) UIImage* attachmentIcon;
#pragma mark - Public methods
/**
Create a new `MXKCellData` object for a new search result cell.
@param searchResult Bulk result returned by MatrixSDK.
@param searchDataSource the `MXKSearchDataSource` object that will use this instance.
@param onComplete a block providing the newly created instance.
*/
+ (void)cellDataWithSearchResult:(MXSearchResult*)searchResult andSearchDataSource:(MXKSearchDataSource*)searchDataSource onComplete:(void (^)(id<MXKSearchCellDataStoring> cellData))onComplete;
@end
@@ -0,0 +1,108 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKDataSource.h"
#import "MXKSearchCellDataStoring.h"
#import "MXKEventFormatter.h"
/**
String identifying the object used to store and prepare the cell data of a result during a message search.
*/
extern NSString *const kMXKSearchCellDataIdentifier;
/**
The data source for `MXKSearchViewController` in case of message search.
Use the `initWithMatrixSession:` constructor to search in all user's rooms.
Use the `initWithRoomId:andMatrixSession: constructor to search in a specific room.
*/
@interface MXKSearchDataSource : MXKDataSource <UITableViewDataSource>
{
@protected
/**
List of results retrieved from the server.
The` MXKSearchDataSource` class stores MXKSearchCellDataStoring objects in it.
*/
NSMutableArray<MXKCellData*> *cellDataArray;
}
/**
The current search.
*/
@property (nonatomic, readonly) NSString *searchText;
/**
The room events filter which is applied during the messages search.
*/
@property (nonatomic) MXRoomEventFilter *roomEventFilter;
/**
Total number of results available on the server.
*/
@property (nonatomic, readonly) NSUInteger serverCount;
/**
The events to display texts formatter.
`MXKCellData` instances can use it to format text.
*/
@property (nonatomic) MXKEventFormatter *eventFormatter;
/**
Flag indicating if there are still results (in the past) to get with paginateBack.
*/
@property (nonatomic, readonly) BOOL canPaginate;
/**
Tell whether the room display name should be displayed in each result cell. NO by default.
*/
@property (nonatomic) BOOL shouldShowRoomDisplayName;
/**
Launch a message search homeserver side.
@discussion The result depends on the 'roomEventFilter' propertie.
@param textPattern the text to search in messages data.
@param force tell whether the search must be launched even if the text pattern is unchanged.
*/
- (void)searchMessages:(NSString*)textPattern force:(BOOL)force;
/**
Load more results from the past.
*/
- (void)paginateBack;
/**
Get the data for the cell at the given index.
@param index the index of the cell in the array
@return the cell data
*/
- (MXKCellData*)cellDataAtIndex:(NSInteger)index;
/**
Convert the results of a homeserver search requests into cells.
This methods is in charge of filling `cellDataArray`.
@param roomEventResults the homeserver response as provided by MatrixSDK.
@param onComplete the block called once complete.
*/
- (void)convertHomeserverResultsIntoCells:(MXSearchRoomEventResults*)roomEventResults onComplete:(dispatch_block_t)onComplete;
@end
@@ -0,0 +1,275 @@
/*
Copyright 2015 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKSearchDataSource.h"
#import "MXKSearchCellData.h"
#pragma mark - Constant definitions
NSString *const kMXKSearchCellDataIdentifier = @"kMXKSearchCellDataIdentifier";
@interface MXKSearchDataSource ()
{
/**
The current search request.
*/
MXHTTPOperation *searchRequest;
/**
Token that can be used to get the next batch of results in the group, if exists.
*/
NSString *nextBatch;
}
@end
@implementation MXKSearchDataSource
- (instancetype)initWithMatrixSession:(MXSession *)mxSession
{
self = [super initWithMatrixSession:mxSession];
if (self)
{
// Set default data and view classes
// Cell data
[self registerCellDataClass:MXKSearchCellData.class forCellIdentifier:kMXKSearchCellDataIdentifier];
// Set default MXEvent -> NSString formatter
_eventFormatter = [[MXKEventFormatter alloc] initWithMatrixSession:mxSession];
_roomEventFilter = [[MXRoomEventFilter alloc] init];
cellDataArray = [NSMutableArray array];
}
return self;
}
- (void)destroy
{
cellDataArray = nil;
_eventFormatter = nil;
_roomEventFilter = nil;
[super destroy];
}
- (void)searchMessages:(NSString*)textPattern force:(BOOL)force
{
if (force || ![_searchText isEqualToString:textPattern])
{
// Reset data before making the new search
if (searchRequest)
{
[searchRequest cancel];
searchRequest = nil;
}
_searchText = textPattern;
_serverCount = 0;
_canPaginate = NO;
nextBatch = nil;
self.state = MXKDataSourceStatePreparing;
[cellDataArray removeAllObjects];
if (textPattern.length)
{
MXLogDebug(@"[MXKSearchDataSource] searchMessages: %@", textPattern);
[self doSearch];
}
else
{
// Refresh table display.
self.state = MXKDataSourceStateReady;
[self.delegate dataSource:self didCellChange:nil];
}
}
}
- (void)paginateBack
{
MXLogDebug(@"[MXKSearchDataSource] paginateBack");
self.state = MXKDataSourceStatePreparing;
[self doSearch];
}
- (MXKCellData*)cellDataAtIndex:(NSInteger)index
{
MXKCellData *cellData;
if (index < cellDataArray.count)
{
cellData = cellDataArray[index];
}
return cellData;
}
- (void)convertHomeserverResultsIntoCells:(MXSearchRoomEventResults*)roomEventResults onComplete:(dispatch_block_t)onComplete
{
// Retrieve the MXKCellData class to manage the data
// Note: MXKSearchDataSource only manages MXKCellData that conforms to MXKSearchCellDataStoring protocol
// see `[registerCellDataClass:forCellIdentifier:]`
Class class = [self cellDataClassForCellIdentifier:kMXKSearchCellDataIdentifier];
dispatch_group_t group = dispatch_group_create();
for (MXSearchResult *result in roomEventResults.results)
{
dispatch_group_enter(group);
[class cellDataWithSearchResult:result andSearchDataSource:self onComplete:^(__autoreleasing id<MXKSearchCellDataStoring> cellData) {
dispatch_group_leave(group);
if (cellData)
{
((id<MXKSearchCellDataStoring>)cellData).shouldShowRoomDisplayName = self.shouldShowRoomDisplayName;
// Use profile information as data to display
MXSearchUserProfile *userProfile = result.context.profileInfo[result.result.sender];
cellData.senderDisplayName = userProfile.displayName;
[self->cellDataArray insertObject:cellData atIndex:0];
}
}];
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
onComplete();
});
}
#pragma mark - Private methods
// Update the MXKDataSource and notify the delegate
- (void)setState:(MXKDataSourceState)newState
{
state = newState;
if (self.delegate)
{
if ([self.delegate respondsToSelector:@selector(dataSource:didStateChange:)])
{
[self.delegate dataSource:self didStateChange:state];
}
}
}
- (void)doSearch
{
// Handle one request at a time
if (searchRequest)
{
return;
}
NSDate *startDate = [NSDate date];
MXWeakify(self);
searchRequest = [self.mxSession.matrixRestClient searchMessagesWithText:_searchText roomEventFilter:_roomEventFilter beforeLimit:0 afterLimit:0 nextBatch:nextBatch success:^(MXSearchRoomEventResults *roomEventResults) {
MXStrongifyAndReturnIfNil(self);
MXLogDebug(@"[MXKSearchDataSource] searchMessages: %@ (%d). Done in %.3fms - Got %tu / %tu messages", self.searchText, self.roomEventFilter.containsURL, [[NSDate date] timeIntervalSinceDate:startDate] * 1000, roomEventResults.results.count, roomEventResults.count);
self->searchRequest = nil;
self->_serverCount = roomEventResults.count;
self->nextBatch = roomEventResults.nextBatch;
self->_canPaginate = (nil != self->nextBatch);
// Process HS response to cells data
MXWeakify(self);
[self convertHomeserverResultsIntoCells:roomEventResults onComplete:^{
MXStrongifyAndReturnIfNil(self);
self.state = MXKDataSourceStateReady;
// Provide changes information to the delegate
NSIndexSet *insertedIndexes;
if (roomEventResults.results.count)
{
insertedIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, roomEventResults.results.count)];
}
[self.delegate dataSource:self didCellChange:insertedIndexes];
}];
} failure:^(NSError *error) {
MXStrongifyAndReturnIfNil(self);
self->searchRequest = nil;
self.state = MXKDataSourceStateFailed;
}];
}
#pragma mark - Override MXKDataSource
- (void)registerCellDataClass:(Class)cellDataClass forCellIdentifier:(NSString *)identifier
{
if ([identifier isEqualToString:kMXKSearchCellDataIdentifier])
{
// Sanity check
NSAssert([cellDataClass conformsToProtocol:@protocol(MXKSearchCellDataStoring)], @"MXKSearchDataSource only manages MXKCellData that conforms to MXKSearchCellDataStoring protocol");
}
[super registerCellDataClass:cellDataClass forCellIdentifier:identifier];
}
- (void)cancelAllRequests
{
if (searchRequest)
{
[searchRequest cancel];
searchRequest = nil;
}
[super cancelAllRequests];
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return cellDataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MXKCellData* cellData = [self cellDataAtIndex:indexPath.row];
NSString *cellIdentifier = [self.delegate cellReuseIdentifierForCellData:cellData];
if (cellIdentifier)
{
UITableViewCell<MXKCellRendering> *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
// Make the bubble display the data
[cell render:cellData];
// Disable any interactions defined in the cell
// because we want [tableView didSelectRowAtIndexPath:] to be called
cell.contentView.userInteractionEnabled = NO;
// Force background color change on selection
cell.selectionStyle = UITableViewCellSelectionStyleDefault;
return cell;
}
// Return a fake cell to prevent app from crashing.
return [[UITableViewCell alloc] init];
}
@end