Merge MatrixKit develop with commit hash: b85b736313bec0592bd1cabc68035d97f5331137

This commit is contained in:
SBiOSoftWhare
2021-12-03 11:47:24 +01:00
parent af65f71177
commit f57941177e
475 changed files with 87437 additions and 0 deletions
@@ -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