4385 lines
175 KiB
Objective-C
4385 lines
175 KiB
Objective-C
/*
|
|
Copyright 2024 New Vector Ltd.
|
|
Copyright 2019 The Matrix.org Foundation C.I.C
|
|
Copyright 2018 New Vector Ltd
|
|
Copyright 2017 Vector Creations Ltd
|
|
Copyright 2015 OpenMarket Ltd
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
Please see LICENSE in the repository root for full details.
|
|
*/
|
|
|
|
#import "MXKRoomDataSource.h"
|
|
|
|
@import MatrixSDK;
|
|
|
|
#import "MXKQueuedEvent.h"
|
|
#import "MXKRoomBubbleTableViewCell.h"
|
|
|
|
#import "MXKRoomBubbleCellData.h"
|
|
|
|
#import "MXKTools.h"
|
|
#import "MXAggregatedReactions+MatrixKit.h"
|
|
|
|
#import "MXKAppSettings.h"
|
|
|
|
#import "GeneratedInterface-Swift.h"
|
|
|
|
const BOOL USE_THREAD_TIMELINE = YES;
|
|
|
|
#pragma mark - Constant definitions
|
|
|
|
NSString *const kMXKRoomBubbleCellDataIdentifier = @"kMXKRoomBubbleCellDataIdentifier";
|
|
|
|
NSString *const kMXKRoomDataSourceSyncStatusChanged = @"kMXKRoomDataSourceSyncStatusChanged";
|
|
NSString *const kMXKRoomDataSourceFailToLoadTimelinePosition = @"kMXKRoomDataSourceFailToLoadTimelinePosition";
|
|
NSString *const kMXKRoomDataSourceTimelineError = @"kMXKRoomDataSourceTimelineError";
|
|
NSString *const kMXKRoomDataSourceTimelineErrorErrorKey = @"kMXKRoomDataSourceTimelineErrorErrorKey";
|
|
|
|
NSString * const MXKRoomDataSourceErrorDomain = @"kMXKRoomDataSourceErrorDomain";
|
|
|
|
typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
|
|
MXKRoomDataSourceErrorResendGeneric = 10001,
|
|
MXKRoomDataSourceErrorResendInvalidMessageType = 10002,
|
|
MXKRoomDataSourceErrorResendInvalidLocalFilePath = 10003,
|
|
};
|
|
|
|
|
|
@interface MXKRoomDataSource ()
|
|
{
|
|
/**
|
|
If the data is not from a live timeline, `initialEventId` is the event in the past
|
|
where the timeline starts.
|
|
*/
|
|
NSString *initialEventId;
|
|
|
|
/**
|
|
Current pagination request (if any)
|
|
*/
|
|
MXHTTPOperation *paginationRequest;
|
|
|
|
/**
|
|
The actual listener related to the current pagination in the timeline.
|
|
*/
|
|
id paginationListener;
|
|
|
|
/**
|
|
The listener to incoming events in the room.
|
|
*/
|
|
id liveEventsListener;
|
|
|
|
/**
|
|
The listener to redaction events in the room.
|
|
*/
|
|
id redactionListener;
|
|
|
|
/**
|
|
The listener to receipts events in the room.
|
|
*/
|
|
id receiptsListener;
|
|
|
|
/**
|
|
The listener to reactions changed in the room.
|
|
*/
|
|
id reactionsChangeListener;
|
|
|
|
/**
|
|
The listener to edits in the room.
|
|
*/
|
|
id eventEditsListener;
|
|
|
|
/**
|
|
Current secondary pagination request (if any)
|
|
*/
|
|
MXHTTPOperation *secondaryPaginationRequest;
|
|
|
|
/**
|
|
The listener to incoming events in the secondary room.
|
|
*/
|
|
id secondaryLiveEventsListener;
|
|
|
|
/**
|
|
The listener to redaction events in the secondary room.
|
|
*/
|
|
id secondaryRedactionListener;
|
|
|
|
/**
|
|
The actual listener related to the current pagination in the secondary timeline.
|
|
*/
|
|
id secondaryPaginationListener;
|
|
|
|
/**
|
|
Mapping between events ids and bubbles.
|
|
*/
|
|
NSMutableDictionary *eventIdToBubbleMap;
|
|
|
|
/**
|
|
Typing notifications listener.
|
|
*/
|
|
id typingNotifListener;
|
|
|
|
/**
|
|
List of members who are typing in the room.
|
|
*/
|
|
NSArray *currentTypingUsers;
|
|
|
|
/**
|
|
Snapshot of the queued events.
|
|
*/
|
|
NSMutableArray *eventsToProcessSnapshot;
|
|
|
|
/**
|
|
Snapshot of the bubbles used during events processing.
|
|
*/
|
|
NSMutableArray<id<MXKRoomBubbleCellDataStoring>> *bubblesSnapshot;
|
|
|
|
/**
|
|
The room being peeked, if any.
|
|
*/
|
|
MXPeekingRoom *peekingRoom;
|
|
|
|
/**
|
|
If any, the non terminated series of collapsable events at the start of self.bubbles.
|
|
(Such series is determined by the cell data of its oldest event).
|
|
*/
|
|
id<MXKRoomBubbleCellDataStoring> collapsableSeriesAtStart;
|
|
|
|
/**
|
|
If any, the non terminated series of collapsable events at the end of self.bubbles.
|
|
(Such series is determined by the cell data of its oldest event).
|
|
*/
|
|
id<MXKRoomBubbleCellDataStoring> collapsableSeriesAtEnd;
|
|
|
|
/**
|
|
Observe UIApplicationSignificantTimeChangeNotification to trigger cell change on time formatting change.
|
|
*/
|
|
id UIApplicationSignificantTimeChangeNotificationObserver;
|
|
|
|
/**
|
|
Observe NSCurrentLocaleDidChangeNotification to trigger cell change on time formatting change.
|
|
*/
|
|
id NSCurrentLocaleDidChangeNotificationObserver;
|
|
|
|
/**
|
|
Observe kMXRoomDidFlushDataNotification to trigger cell change when existing room history has been flushed during server sync.
|
|
*/
|
|
id roomDidFlushDataNotificationObserver;
|
|
|
|
/**
|
|
Observe kMXRoomDidUpdateUnreadNotification to refresh unread counters.
|
|
*/
|
|
id roomDidUpdateUnreadNotificationObserver;
|
|
|
|
/**
|
|
Emote slash command prefix @"/me "
|
|
*/
|
|
NSString *emoteMessageSlashCommandPrefix;
|
|
}
|
|
|
|
/**
|
|
Indicate to stop back-paginating when finding an un-decryptable event as previous event.
|
|
It is used to hide pre join UTD events before joining the room.
|
|
*/
|
|
@property (nonatomic, assign) BOOL shouldPreventBackPaginationOnPreviousUTDEvent;
|
|
|
|
/**
|
|
Indicate to stop back-paginating.
|
|
*/
|
|
@property (nonatomic, assign) BOOL shouldStopBackPagination;
|
|
|
|
@property (nonatomic, readwrite) MXRoom *room;
|
|
@property (nonatomic, readwrite) MXThread *thread;
|
|
|
|
@property (nonatomic, readwrite) MXRoom *secondaryRoom;
|
|
@property (nonatomic, strong) id<MXEventTimeline> secondaryTimeline;
|
|
@property (nonatomic, readwrite) NSString *threadId;
|
|
|
|
@end
|
|
|
|
@implementation MXKRoomDataSource
|
|
|
|
+ (void)loadRoomDataSourceWithRoomId:(NSString*)roomId threadId:(NSString*)threadId andMatrixSession:(MXSession*)mxSession onComplete:(void (^)(id roomDataSource))onComplete
|
|
{
|
|
MXKRoomDataSource *roomDataSource = [[self alloc] initWithRoomId:roomId andMatrixSession:mxSession threadId:threadId];
|
|
[self ensureSessionStateForDataSource:roomDataSource initialEventId:nil andMatrixSession:mxSession onComplete:onComplete];
|
|
}
|
|
|
|
+ (void)loadRoomDataSourceWithRoomId:(NSString*)roomId initialEventId:(NSString*)initialEventId threadId:(NSString*)threadId andMatrixSession:(MXSession*)mxSession onComplete:(void (^)(id roomDataSource))onComplete
|
|
{
|
|
MXKRoomDataSource *roomDataSource = [[self alloc] initWithRoomId:roomId initialEventId:initialEventId threadId:threadId andMatrixSession:mxSession];
|
|
[self ensureSessionStateForDataSource:roomDataSource initialEventId:initialEventId andMatrixSession:mxSession onComplete:onComplete];
|
|
}
|
|
|
|
+ (void)loadRoomDataSourceWithPeekingRoom:(MXPeekingRoom*)peekingRoom andInitialEventId:(NSString*)initialEventId onComplete:(void (^)(id roomDataSource))onComplete
|
|
{
|
|
MXKRoomDataSource *roomDataSource = [[self alloc] initWithPeekingRoom:peekingRoom andInitialEventId:initialEventId];
|
|
[self finalizeRoomDataSource:roomDataSource onComplete:onComplete];
|
|
}
|
|
|
|
/// Ensure session state to be store data ready for the roomDataSource.
|
|
+ (void)ensureSessionStateForDataSource:(MXKRoomDataSource*)roomDataSource initialEventId:(NSString*)initialEventId andMatrixSession:(MXSession*)mxSession onComplete:(void (^)(id roomDataSource))onComplete
|
|
{
|
|
// if store is not ready, roomDataSource.room will be nil. So onComplete block will never be called.
|
|
// In order to successfully fetch the room, we should wait for store to be ready.
|
|
if (mxSession.state >= MXSessionStateStoreDataReady)
|
|
{
|
|
[self finalizeRoomDataSource:roomDataSource onComplete:onComplete];
|
|
}
|
|
else
|
|
{
|
|
// wait for session state to be store data ready
|
|
__block id sessionStateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionStateDidChangeNotification object:mxSession queue:nil usingBlock:^(NSNotification * _Nonnull note) {
|
|
if (mxSession.state >= MXSessionStateStoreDataReady)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:sessionStateObserver];
|
|
[self finalizeRoomDataSource:roomDataSource onComplete:onComplete];
|
|
}
|
|
}];
|
|
}
|
|
}
|
|
|
|
+ (void)finalizeRoomDataSource:(MXKRoomDataSource*)roomDataSource onComplete:(void (^)(id roomDataSource))onComplete
|
|
{
|
|
if (roomDataSource)
|
|
{
|
|
[roomDataSource finalizeInitialization];
|
|
|
|
// Asynchronously preload data here so that the data will be ready later
|
|
// to synchronously respond to that request
|
|
|
|
if (USE_THREAD_TIMELINE)
|
|
{
|
|
if (roomDataSource.threadId)
|
|
{
|
|
[roomDataSource.thread liveTimeline:^(id<MXEventTimeline> _Nonnull liveTimeline) {
|
|
[liveTimeline resetPagination];
|
|
onComplete(roomDataSource);
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
[roomDataSource.room liveTimeline:^(id<MXEventTimeline> liveTimeline) {
|
|
[liveTimeline resetPagination];
|
|
onComplete(roomDataSource);
|
|
}];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[roomDataSource.room liveTimeline:^(id<MXEventTimeline> liveTimeline) {
|
|
[liveTimeline resetPagination];
|
|
onComplete(roomDataSource);
|
|
}];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (instancetype)initWithRoomId:(NSString *)roomId andMatrixSession:(MXSession *)matrixSession threadId:(NSString *)threadId
|
|
{
|
|
self = [super initWithMatrixSession:matrixSession];
|
|
if (self)
|
|
{
|
|
MXLogVerbose(@"[MXKRoomDataSource][%p] initWithRoomId: %@", self, roomId);
|
|
|
|
_roomId = roomId;
|
|
_threadId = threadId;
|
|
_secondaryRoomEventTypes = @[
|
|
kMXEventTypeStringCallInvite,
|
|
kMXEventTypeStringCallCandidates,
|
|
kMXEventTypeStringCallAnswer,
|
|
kMXEventTypeStringCallSelectAnswer,
|
|
kMXEventTypeStringCallHangup,
|
|
kMXEventTypeStringCallReject,
|
|
kMXEventTypeStringCallNegotiate,
|
|
kMXEventTypeStringCallReplaces,
|
|
kMXEventTypeStringCallRejectReplacement
|
|
];
|
|
NSString *virtualRoomId = [matrixSession virtualRoomOf:_roomId];
|
|
if (virtualRoomId)
|
|
{
|
|
_secondaryRoomId = virtualRoomId;
|
|
}
|
|
_isLive = YES;
|
|
bubbles = [NSMutableArray array];
|
|
eventsToProcess = [NSMutableArray array];
|
|
eventIdToBubbleMap = [NSMutableDictionary dictionary];
|
|
|
|
_filterMessagesWithURL = NO;
|
|
|
|
emoteMessageSlashCommandPrefix = [NSString stringWithFormat:@"%@ ", [MXKSlashCommandsHelper commandNameFor:MXKSlashCommandEmote]];
|
|
|
|
// Set default data and view classes
|
|
// Cell data
|
|
[self registerCellDataClass:MXKRoomBubbleCellData.class forCellIdentifier:kMXKRoomBubbleCellDataIdentifier];
|
|
|
|
// Set default MXEvent -> NSString formatter
|
|
self.eventFormatter = [[MXKEventFormatter alloc] initWithMatrixSession:self.mxSession];
|
|
// Apply here the event types filter to display only the wanted event types.
|
|
self.eventFormatter.eventTypesFilterForMessages = [MXKAppSettings standardAppSettings].eventsFilterForMessages;
|
|
|
|
// display the read receips by default
|
|
self.showBubbleReceipts = YES;
|
|
|
|
// show the read marker by default
|
|
self.showReadMarker = YES;
|
|
|
|
// Disable typing notification in cells by default.
|
|
self.showTypingNotifications = NO;
|
|
|
|
self.useCustomDateTimeLabel = NO;
|
|
self.useCustomReceipts = NO;
|
|
self.useCustomUnsentButton = NO;
|
|
|
|
_maxBackgroundCachedBubblesCount = MXKROOMDATASOURCE_CACHED_BUBBLES_COUNT_THRESHOLD;
|
|
_paginationLimitAroundInitialEvent = MXKROOMDATASOURCE_PAGINATION_LIMIT_AROUND_INITIAL_EVENT;
|
|
|
|
// Observe UIApplicationSignificantTimeChangeNotification to refresh bubbles if date/time are shown.
|
|
// UIApplicationSignificantTimeChangeNotification is posted if DST is updated, carrier time is updated
|
|
UIApplicationSignificantTimeChangeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationSignificantTimeChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
|
[self onDateTimeFormatUpdate];
|
|
}];
|
|
|
|
// Observe NSCurrentLocaleDidChangeNotification to refresh bubbles if date/time are shown.
|
|
// NSCurrentLocaleDidChangeNotification is triggered when the time swicthes to AM/PM to 24h time format
|
|
NSCurrentLocaleDidChangeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSCurrentLocaleDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
|
|
|
[self onDateTimeFormatUpdate];
|
|
|
|
}];
|
|
|
|
// Listen to the event sent state changes
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(eventDidChangeSentState:) name:kMXEventDidChangeSentStateNotification object:nil];
|
|
// Listen to events decrypted
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(eventDidDecrypt:) name:kMXEventDidDecryptNotification object:nil];
|
|
// Listen to virtual rooms change
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(virtualRoomsDidChange:) name:kMXSessionVirtualRoomsDidChangeNotification object:matrixSession];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)initWithRoomId:(NSString*)roomId initialEventId:(NSString*)initialEventId2 threadId:(NSString*)threadId andMatrixSession:(MXSession*)mxSession
|
|
{
|
|
self = [self initWithRoomId:roomId andMatrixSession:mxSession threadId:threadId];
|
|
if (self)
|
|
{
|
|
if (initialEventId2)
|
|
{
|
|
initialEventId = initialEventId2;
|
|
_isLive = NO;
|
|
}
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)initWithPeekingRoom:(MXPeekingRoom*)peekingRoom2 andInitialEventId:(NSString*)theInitialEventId
|
|
{
|
|
self = [self initWithRoomId:peekingRoom2.roomId initialEventId:theInitialEventId threadId:nil andMatrixSession:peekingRoom2.mxSession];
|
|
if (self)
|
|
{
|
|
peekingRoom = peekingRoom2;
|
|
_isPeeking = YES;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[self unregisterEventEditsListener];
|
|
[self unregisterScanManagerNotifications];
|
|
[self unregisterReactionsChangeListener];
|
|
}
|
|
|
|
- (MXRoomState *)roomState
|
|
{
|
|
// @TODO(async-state): Just here for dev
|
|
NSAssert(_timeline.state, @"[MXKRoomDataSource] Room state must be preloaded before accessing to MXKRoomDataSource.roomState");
|
|
return _timeline.state;
|
|
}
|
|
|
|
- (void)onDateTimeFormatUpdate
|
|
{
|
|
// update the date and the time formatters
|
|
[self.eventFormatter initDateTimeFormatters];
|
|
|
|
// refresh the UI if it is required
|
|
if (self.showBubblesDateTime && self.delegate)
|
|
{
|
|
// Reload all the table
|
|
[self.delegate dataSource:self didCellChange:nil];
|
|
}
|
|
}
|
|
|
|
- (void)markAllAsRead
|
|
{
|
|
[_room.summary markAllAsRead];
|
|
}
|
|
|
|
- (void)limitMemoryUsage:(NSInteger)maxBubbleNb
|
|
{
|
|
NSInteger bubbleCount;
|
|
@synchronized(bubbles)
|
|
{
|
|
bubbleCount = bubbles.count;
|
|
}
|
|
|
|
if (bubbleCount > maxBubbleNb)
|
|
{
|
|
// Do nothing if some local echoes are in progress.
|
|
NSArray<MXEvent*>* outgoingMessages = _room.outgoingMessages;
|
|
|
|
for (NSInteger index = 0; index < outgoingMessages.count; index++)
|
|
{
|
|
MXEvent *outgoingMessage = [outgoingMessages objectAtIndex:index];
|
|
|
|
if (outgoingMessage.sentState == MXEventSentStateSending ||
|
|
outgoingMessage.sentState == MXEventSentStatePreparing ||
|
|
outgoingMessage.sentState == MXEventSentStateEncrypting ||
|
|
outgoingMessage.sentState == MXEventSentStateUploading)
|
|
{
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] cancel limitMemoryUsage because some messages are being sent", self);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Reset the room data source (return in initial state: minimum memory usage).
|
|
[self reload];
|
|
}
|
|
}
|
|
|
|
- (void)reset
|
|
{
|
|
if (roomDidFlushDataNotificationObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:roomDidFlushDataNotificationObserver];
|
|
roomDidFlushDataNotificationObserver = nil;
|
|
}
|
|
|
|
if (roomDidUpdateUnreadNotificationObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:roomDidUpdateUnreadNotificationObserver];
|
|
roomDidUpdateUnreadNotificationObserver = nil;
|
|
}
|
|
|
|
if (paginationRequest)
|
|
{
|
|
// We have to remove here the listener. A new pagination request may be triggered whereas the cancellation of this one is in progress
|
|
[_timeline removeListener:paginationListener];
|
|
paginationListener = nil;
|
|
|
|
[paginationRequest cancel];
|
|
paginationRequest = nil;
|
|
}
|
|
|
|
if (secondaryPaginationRequest)
|
|
{
|
|
// We have to remove here the listener. A new pagination request may be triggered whereas the cancellation of this one is in progress
|
|
[_secondaryTimeline removeListener:secondaryPaginationListener];
|
|
secondaryPaginationListener = nil;
|
|
|
|
[secondaryPaginationRequest cancel];
|
|
secondaryPaginationRequest = nil;
|
|
}
|
|
|
|
if (_room && liveEventsListener)
|
|
{
|
|
[_timeline removeListener:liveEventsListener];
|
|
liveEventsListener = nil;
|
|
|
|
[_timeline removeListener:redactionListener];
|
|
redactionListener = nil;
|
|
|
|
[_timeline removeListener:receiptsListener];
|
|
receiptsListener = nil;
|
|
}
|
|
|
|
if (_secondaryRoom && secondaryLiveEventsListener)
|
|
{
|
|
[_secondaryTimeline removeListener:secondaryLiveEventsListener];
|
|
secondaryLiveEventsListener = nil;
|
|
|
|
[_secondaryTimeline removeListener:secondaryRedactionListener];
|
|
secondaryRedactionListener = nil;
|
|
}
|
|
|
|
if (_room && typingNotifListener)
|
|
{
|
|
[_timeline removeListener:typingNotifListener];
|
|
typingNotifListener = nil;
|
|
}
|
|
currentTypingUsers = nil;
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXRoomInitialSyncNotification object:nil];
|
|
|
|
@synchronized(eventsToProcess)
|
|
{
|
|
MXLogVerbose(@"[MXKRoomDataSource][%p] Reset eventsToProcess", self);
|
|
[eventsToProcess removeAllObjects];
|
|
}
|
|
|
|
// Suspend the reset operation if some events is under processing
|
|
@synchronized(eventsToProcessSnapshot)
|
|
{
|
|
eventsToProcessSnapshot = nil;
|
|
bubblesSnapshot = nil;
|
|
|
|
@synchronized(bubbles)
|
|
{
|
|
for (id<MXKRoomBubbleCellDataStoring> bubble in bubbles) {
|
|
bubble.prevCollapsableCellData = nil;
|
|
bubble.nextCollapsableCellData = nil;
|
|
}
|
|
[bubbles removeAllObjects];
|
|
}
|
|
|
|
@synchronized(eventIdToBubbleMap)
|
|
{
|
|
[eventIdToBubbleMap removeAllObjects];
|
|
}
|
|
|
|
self.room = nil;
|
|
self.thread = nil;
|
|
self.secondaryRoom = nil;
|
|
}
|
|
|
|
_serverSyncEventCount = 0;
|
|
}
|
|
|
|
- (void)reload
|
|
{
|
|
[self reloadNotifying:YES];
|
|
}
|
|
|
|
- (void)reloadNotifying:(BOOL)notify
|
|
{
|
|
MXLogVerbose(@"[MXKRoomDataSource][%p] Reload - room id: %@", self, _roomId);
|
|
|
|
[self setState:MXKDataSourceStatePreparing];
|
|
|
|
[self reset];
|
|
|
|
// Reload
|
|
[self didMXSessionStateChange];
|
|
|
|
// Notify the delegate to refresh the tableview
|
|
if (notify && self.delegate)
|
|
{
|
|
[self.delegate dataSource:self didCellChange:nil];
|
|
}
|
|
}
|
|
|
|
- (void)destroy
|
|
{
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] Destroy - room id: %@ - thread id: %@", self, _roomId, _threadId);
|
|
|
|
[self unregisterScanManagerNotifications];
|
|
[self unregisterReactionsChangeListener];
|
|
[self unregisterEventEditsListener];
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidChangeSentStateNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidDecryptNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidChangeIdentifierNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXSessionVirtualRoomsDidChangeNotification object:nil];
|
|
|
|
if (NSCurrentLocaleDidChangeNotificationObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:NSCurrentLocaleDidChangeNotificationObserver];
|
|
NSCurrentLocaleDidChangeNotificationObserver = nil;
|
|
}
|
|
|
|
if (UIApplicationSignificantTimeChangeNotificationObserver)
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:UIApplicationSignificantTimeChangeNotificationObserver];
|
|
UIApplicationSignificantTimeChangeNotificationObserver = nil;
|
|
}
|
|
|
|
// If the room data source was used to peek into a room, stop the events stream on this room
|
|
if (peekingRoom)
|
|
{
|
|
[_room.mxSession stopPeeking:peekingRoom];
|
|
}
|
|
|
|
[self reset];
|
|
|
|
self.eventFormatter = nil;
|
|
|
|
eventsToProcess = nil;
|
|
bubbles = nil;
|
|
eventIdToBubbleMap = nil;
|
|
|
|
[_timeline destroy];
|
|
[_secondaryTimeline destroy];
|
|
|
|
[super destroy];
|
|
}
|
|
|
|
- (void)didMXSessionStateChange
|
|
{
|
|
if (MXSessionStateStoreDataReady <= self.mxSession.state)
|
|
{
|
|
if (USE_THREAD_TIMELINE)
|
|
{
|
|
if (_threadId)
|
|
{
|
|
[self initializeTimelineForThread];
|
|
}
|
|
else
|
|
{
|
|
[self initializeTimelineForRoom];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[self initializeTimelineForRoom];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)initializeTimelineForRoom
|
|
{
|
|
// Check whether the room is not already set
|
|
if (!_room)
|
|
{
|
|
// Are we peeking into a random room or displaying a room the user is part of?
|
|
if (peekingRoom)
|
|
{
|
|
self.room = peekingRoom;
|
|
}
|
|
else
|
|
{
|
|
self.room = [self.mxSession roomWithRoomId:_roomId];
|
|
}
|
|
|
|
if (_room)
|
|
{
|
|
// This is the time to set up the timeline according to the called init method
|
|
if (_isLive)
|
|
{
|
|
// LIVE
|
|
MXWeakify(self);
|
|
[_room liveTimeline:^(id<MXEventTimeline> liveTimeline) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
self->_timeline = liveTimeline;
|
|
|
|
// Only one pagination process can be done at a time by an MXRoom object.
|
|
// This assumption is satisfied by MatrixKit. Only MXRoomDataSource does it.
|
|
[self.timeline resetPagination];
|
|
|
|
// Observe room history flush (sync with limited timeline, or state event redaction)
|
|
self->roomDidFlushDataNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXRoomDidFlushDataNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
|
|
|
|
MXRoom *room = notif.object;
|
|
if (self.mxSession == room.mxSession && ([self.roomId isEqualToString:room.roomId] ||
|
|
([self.secondaryRoomId isEqualToString:room.roomId])))
|
|
{
|
|
// The existing room history has been flushed during server sync because a gap has been observed between local and server storage.
|
|
[self reload];
|
|
}
|
|
|
|
}];
|
|
|
|
// Add the event listeners, by considering all the event types (the event filtering is applying by the event formatter),
|
|
// except if only the events with a url key in their content must be handled.
|
|
[self refreshEventListeners:(self.filterMessagesWithURL ? @[kMXEventTypeStringRoomMessage] : [MXKAppSettings standardAppSettings].allEventTypesForMessages)];
|
|
|
|
// display typing notifications is optional
|
|
// the inherited class can manage them by its own.
|
|
if (self.showTypingNotifications)
|
|
{
|
|
// Register on typing notif
|
|
[self listenTypingNotifications];
|
|
}
|
|
|
|
// Manage unsent messages
|
|
[self handleUnsentMessages];
|
|
|
|
// Update here data source state if it is not already ready
|
|
if (!self->_secondaryRoomId)
|
|
{
|
|
[self setState:MXKDataSourceStateReady];
|
|
}
|
|
|
|
// Check user membership in this room
|
|
MXMembership membership = self.room.summary.membership;
|
|
if (membership == MXMembershipUnknown || membership == MXMembershipInvite)
|
|
{
|
|
// Here the initial sync is not ended or the room is a pending invitation.
|
|
// Note: In case of invitation, a full sync will be triggered if the user joins this room.
|
|
|
|
// We have to observe here 'kMXRoomInitialSyncNotification' to reload room data when room sync is done.
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXRoomInitialSynced:) name:kMXRoomInitialSyncNotification object:self.room];
|
|
}
|
|
}];
|
|
|
|
if (!_secondaryRoom && _secondaryRoomId)
|
|
{
|
|
_secondaryRoom = [self.mxSession roomWithRoomId:_secondaryRoomId];
|
|
|
|
if (_secondaryRoom)
|
|
{
|
|
MXWeakify(self);
|
|
[_secondaryRoom liveTimeline:^(id<MXEventTimeline> liveTimeline) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
self->_secondaryTimeline = liveTimeline;
|
|
|
|
// Only one pagination process can be done at a time by an MXRoom object.
|
|
// This assumption is satisfied by MatrixKit. Only MXRoomDataSource does it.
|
|
[self.secondaryTimeline resetPagination];
|
|
|
|
// Add the secondary event listeners, by considering the event types in self.secondaryRoomEventTypes
|
|
[self refreshSecondaryEventListeners:self.secondaryRoomEventTypes];
|
|
|
|
// Update here data source state if it is not already ready
|
|
[self setState:MXKDataSourceStateReady];
|
|
|
|
// Check user membership in the secondary room
|
|
MXMembership membership = self.secondaryRoom.summary.membership;
|
|
if (membership == MXMembershipUnknown || membership == MXMembershipInvite)
|
|
{
|
|
// Here the initial sync is not ended or the room is a pending invitation.
|
|
// Note: In case of invitation, a full sync will be triggered if the user joins this room.
|
|
|
|
// We have to observe here 'kMXRoomInitialSyncNotification' to reload room data when room sync is done.
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didMXRoomInitialSynced:) name:kMXRoomInitialSyncNotification object:self.secondaryRoom];
|
|
}
|
|
}];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Past timeline
|
|
// Less things need to configured
|
|
_timeline = [_room timelineOnEvent:initialEventId];
|
|
|
|
// Refresh the event listeners. Note: events for past timelines come only from pagination request
|
|
[self refreshEventListeners:nil];
|
|
|
|
MXWeakify(self);
|
|
|
|
// Preload the state and some messages around the initial event
|
|
[_timeline resetPaginationAroundInitialEventWithLimit:_paginationLimitAroundInitialEvent success:^{
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
// Do a "classic" reset. The room view controller will paginate
|
|
// from the events stored in the timeline store
|
|
[self.timeline resetPagination];
|
|
|
|
// Update here data source state if it is not already ready
|
|
[self setState:MXKDataSourceStateReady];
|
|
|
|
} failure:^(NSError *error) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] Failed to resetPaginationAroundInitialEventWithLimit", self);
|
|
|
|
// Notify the error
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKRoomDataSourceTimelineError
|
|
object:self
|
|
userInfo:@{
|
|
kMXKRoomDataSourceTimelineErrorErrorKey: error
|
|
}];
|
|
}];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] Warning: The user does not know the room %@", self, _roomId);
|
|
|
|
// Update here data source state if it is not already ready
|
|
[self setState:MXKDataSourceStateFailed];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)initializeTimelineForThread
|
|
{
|
|
// Check whether the thread is not already set
|
|
if (_thread && self.state == MXKDataSourceStateReady)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_thread = [self.mxSession.threadingService threadWithId:_threadId];
|
|
|
|
if (!_thread)
|
|
{
|
|
// there is not a thread yet available, this will be a new thread
|
|
_thread = [self.mxSession.threadingService createTempThreadWithId:_threadId roomId:_roomId];
|
|
}
|
|
|
|
if (!_room)
|
|
{
|
|
// also hold a reference to the room
|
|
_room = [self.mxSession roomWithRoomId:_roomId];
|
|
}
|
|
|
|
if (_thread)
|
|
{
|
|
if (_isLive)
|
|
{
|
|
[_thread liveTimeline:^(id<MXEventTimeline> _Nonnull liveTimeline) {
|
|
self->_timeline = liveTimeline;
|
|
|
|
// Only one pagination process can be done at a time by an MXThread object.
|
|
// This assumption is satisfied by MXRoomDataSource.
|
|
[self.timeline resetPagination];
|
|
|
|
// Add the event listeners, by considering all the event types (the event filtering is applying by the event formatter),
|
|
// except if only the events with a url key in their content must be handled.
|
|
[self refreshEventListeners:(self.filterMessagesWithURL ? @[kMXEventTypeStringRoomMessage] : [MXKAppSettings standardAppSettings].allEventTypesForMessages)];
|
|
|
|
// Manage unsent messages
|
|
[self handleUnsentMessages];
|
|
|
|
[self setState:MXKDataSourceStateReady];
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
// Past timeline
|
|
// Less things need to configured
|
|
_timeline = [_thread timelineOnEvent:initialEventId];
|
|
|
|
// Refresh the event listeners. Note: events for past timelines come only from pagination request
|
|
[self refreshEventListeners:nil];
|
|
|
|
MXWeakify(self);
|
|
|
|
// Preload the state and some messages around the initial event
|
|
[_timeline resetPaginationAroundInitialEventWithLimit:_paginationLimitAroundInitialEvent success:^{
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
// Do a "classic" reset. The room view controller will paginate
|
|
// from the events stored in the timeline store
|
|
[self.timeline resetPagination];
|
|
|
|
// Update here data source state if it is not already ready
|
|
[self setState:MXKDataSourceStateReady];
|
|
|
|
} failure:^(NSError *error) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] Failed to resetPaginationAroundInitialEventWithLimit", self);
|
|
|
|
// Notify the error
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKRoomDataSourceTimelineError
|
|
object:self
|
|
userInfo:@{
|
|
kMXKRoomDataSourceTimelineErrorErrorKey: error
|
|
}];
|
|
}];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] Warning: The user does not know the thread %@", self, _threadId);
|
|
|
|
// Update here data source state if it is not already ready
|
|
[self setState:MXKDataSourceStateFailed];
|
|
}
|
|
}
|
|
|
|
- (NSArray *)attachmentsWithThumbnail
|
|
{
|
|
NSMutableArray *attachments = [NSMutableArray array];
|
|
|
|
@synchronized(bubbles)
|
|
{
|
|
for (id<MXKRoomBubbleCellDataStoring> bubbleData in bubbles)
|
|
{
|
|
if (bubbleData.isAttachmentWithThumbnail && bubbleData.attachment.type != MXKAttachmentTypeSticker && !bubbleData.showAntivirusScanStatus)
|
|
{
|
|
[attachments addObject:bubbleData.attachment];
|
|
}
|
|
}
|
|
}
|
|
|
|
return attachments;
|
|
}
|
|
|
|
- (NSAttributedString *)partialAttributedTextMessage
|
|
{
|
|
return _room.partialAttributedTextMessage;
|
|
}
|
|
|
|
- (void)setPartialAttributedTextMessage:(NSAttributedString *)partialAttributedTextMessage
|
|
{
|
|
_room.partialAttributedTextMessage = partialAttributedTextMessage;
|
|
}
|
|
|
|
- (void)refreshEventListeners:(NSArray *)liveEventTypesFilterForMessages
|
|
{
|
|
// Remove the existing listeners
|
|
if (liveEventsListener)
|
|
{
|
|
[_timeline removeListener:liveEventsListener];
|
|
[_timeline removeListener:redactionListener];
|
|
[_timeline removeListener:receiptsListener];
|
|
}
|
|
|
|
// Listen to live events only for live timeline
|
|
// Events for past timelines come only from pagination request
|
|
if (_isLive)
|
|
{
|
|
// Register a new one with the requested filter
|
|
MXWeakify(self);
|
|
liveEventsListener = [_timeline listenToEventsOfTypes:liveEventTypesFilterForMessages onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
if (MXTimelineDirectionForwards == direction)
|
|
{
|
|
if (event.eventType == MXEventTypeRoomMember && event.isUserProfileChange)
|
|
{
|
|
[self refreshProfilesIfNeeded];
|
|
}
|
|
|
|
// Check for local echo suppression
|
|
MXEvent *localEcho;
|
|
if (self.room.outgoingMessages.count && [event.sender isEqualToString:self.mxSession.myUser.userId])
|
|
{
|
|
localEcho = [self.room pendingLocalEchoRelatedToEvent:event];
|
|
if (localEcho)
|
|
{
|
|
// Check whether the local echo has a timestamp (in this case, it is replaced with the actual event).
|
|
if (localEcho.originServerTs != kMXUndefinedTimestamp)
|
|
{
|
|
// Replace the local echo by the true event sent by the homeserver
|
|
[self replaceEvent:localEcho withEvent:event];
|
|
}
|
|
else
|
|
{
|
|
// Remove the local echo, and process independently the true event.
|
|
[self replaceEvent:localEcho withEvent:nil];
|
|
localEcho = nil;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (self.secondaryRoom)
|
|
{
|
|
[self reloadNotifying:NO];
|
|
}
|
|
else if (nil == localEcho)
|
|
{
|
|
// Process here incoming events, and outgoing events sent from another device.
|
|
if (self.threadId == nil && event.isInThread)
|
|
{
|
|
NSInteger index = [self indexOfCellDataWithEventId:event.relatesTo.eventId];
|
|
if (index != NSNotFound)
|
|
{
|
|
[self reloadNotifying:NO];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[self queueEventForProcessing:event withRoomState:roomState direction:MXTimelineDirectionForwards];
|
|
[self processQueuedEvents:nil];
|
|
}
|
|
}
|
|
}
|
|
}];
|
|
|
|
receiptsListener = [_timeline listenToEventsOfTypes:@[kMXEventTypeStringReceipt] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) {
|
|
|
|
if (MXTimelineDirectionForwards == direction)
|
|
{
|
|
// Handle this read receipt
|
|
[self didReceiveReceiptEvent:event roomState:roomState];
|
|
}
|
|
}];
|
|
}
|
|
|
|
// Register a listener to handle redaction which can affect live and past timelines
|
|
MXWeakify(self);
|
|
redactionListener = [_timeline listenToEventsOfTypes:@[kMXEventTypeStringRoomRedaction] onEvent:^(MXEvent *redactionEvent, MXTimelineDirection direction, MXRoomState *roomState) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
// Consider only live redaction events
|
|
if (direction == MXTimelineDirectionForwards)
|
|
{
|
|
// Do the processing on the processing queue
|
|
dispatch_async(MXKRoomDataSource.processingQueue, ^{
|
|
|
|
// Check whether a message contains the redacted event
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData = [self cellDataOfEventWithEventId:redactionEvent.redacts];
|
|
if (bubbleData)
|
|
{
|
|
BOOL shouldRemoveBubbleData = NO;
|
|
BOOL hasChanged = NO;
|
|
MXEvent *redactedEvent = nil;
|
|
|
|
@synchronized (bubbleData)
|
|
{
|
|
// Retrieve the original event to redact it
|
|
NSArray *events = bubbleData.events;
|
|
|
|
for (MXEvent *event in events)
|
|
{
|
|
if ([event.eventId isEqualToString:redactionEvent.redacts])
|
|
{
|
|
// Check whether the event was not already redacted (Redaction may be handled by event timeline too).
|
|
if (!event.isRedactedEvent)
|
|
{
|
|
redactedEvent = [event prune];
|
|
redactedEvent.redactedBecause = redactionEvent.JSONDictionary;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (redactedEvent)
|
|
{
|
|
// Update bubble data
|
|
NSUInteger remainingEvents = [bubbleData updateEvent:redactionEvent.redacts withEvent:redactedEvent];
|
|
|
|
[self refreshRepliesWithUpdatedEventId:redactedEvent.eventId];
|
|
|
|
hasChanged = YES;
|
|
|
|
// Remove the bubble if there is no more events
|
|
shouldRemoveBubbleData = (remainingEvents == 0);
|
|
}
|
|
}
|
|
|
|
// Check whether the bubble should be removed
|
|
if (shouldRemoveBubbleData)
|
|
{
|
|
[self removeCellData:bubbleData];
|
|
}
|
|
|
|
if (hasChanged)
|
|
{
|
|
// Update the delegate on main thread
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
if (self.delegate)
|
|
{
|
|
[self.delegate dataSource:self didCellChange:nil];
|
|
}
|
|
|
|
});
|
|
}
|
|
}
|
|
|
|
});
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)refreshSecondaryEventListeners:(NSArray *)liveEventTypesFilterForMessages
|
|
{
|
|
// Remove the existing listeners
|
|
if (secondaryLiveEventsListener)
|
|
{
|
|
[_secondaryTimeline removeListener:secondaryLiveEventsListener];
|
|
[_secondaryTimeline removeListener:secondaryRedactionListener];
|
|
}
|
|
|
|
// Listen to live events only for live timeline
|
|
// Events for past timelines come only from pagination request
|
|
if (_isLive)
|
|
{
|
|
// Register a new one with the requested filter
|
|
MXWeakify(self);
|
|
secondaryLiveEventsListener = [_secondaryTimeline listenToEventsOfTypes:liveEventTypesFilterForMessages onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) {
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
if (MXTimelineDirectionForwards == direction)
|
|
{
|
|
// Check for local echo suppression
|
|
MXEvent *localEcho;
|
|
if (self.secondaryRoom.outgoingMessages.count && [event.sender isEqualToString:self.mxSession.myUserId])
|
|
{
|
|
localEcho = [self.secondaryRoom pendingLocalEchoRelatedToEvent:event];
|
|
if (localEcho)
|
|
{
|
|
// Check whether the local echo has a timestamp (in this case, it is replaced with the actual event).
|
|
if (localEcho.originServerTs != kMXUndefinedTimestamp)
|
|
{
|
|
// Replace the local echo by the true event sent by the homeserver
|
|
[self replaceEvent:localEcho withEvent:event];
|
|
}
|
|
else
|
|
{
|
|
// Remove the local echo, and process independently the true event.
|
|
[self replaceEvent:localEcho withEvent:nil];
|
|
localEcho = nil;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nil == localEcho)
|
|
{
|
|
// Process here incoming events, and outgoing events sent from another device.
|
|
[self queueEventForProcessing:event withRoomState:roomState direction:MXTimelineDirectionForwards];
|
|
[self processQueuedEvents:nil];
|
|
}
|
|
}
|
|
}];
|
|
|
|
}
|
|
|
|
// Register a listener to handle redaction which can affect live and past timelines
|
|
secondaryRedactionListener = [_secondaryTimeline listenToEventsOfTypes:@[kMXEventTypeStringRoomRedaction] onEvent:^(MXEvent *redactionEvent, MXTimelineDirection direction, MXRoomState *roomState) {
|
|
|
|
// Consider only live redaction events
|
|
if (direction == MXTimelineDirectionForwards)
|
|
{
|
|
// Do the processing on the processing queue
|
|
dispatch_async(MXKRoomDataSource.processingQueue, ^{
|
|
|
|
// Check whether a message contains the redacted event
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData = [self cellDataOfEventWithEventId:redactionEvent.redacts];
|
|
if (bubbleData)
|
|
{
|
|
BOOL shouldRemoveBubbleData = NO;
|
|
BOOL hasChanged = NO;
|
|
MXEvent *redactedEvent = nil;
|
|
|
|
@synchronized (bubbleData)
|
|
{
|
|
// Retrieve the original event to redact it
|
|
NSArray *events = bubbleData.events;
|
|
|
|
for (MXEvent *event in events)
|
|
{
|
|
if ([event.eventId isEqualToString:redactionEvent.redacts])
|
|
{
|
|
// Check whether the event was not already redacted (Redaction may be handled by event timeline too).
|
|
if (!event.isRedactedEvent)
|
|
{
|
|
redactedEvent = [event prune];
|
|
redactedEvent.redactedBecause = redactionEvent.JSONDictionary;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (redactedEvent)
|
|
{
|
|
// Update bubble data
|
|
NSUInteger remainingEvents = [bubbleData updateEvent:redactionEvent.redacts withEvent:redactedEvent];
|
|
|
|
hasChanged = YES;
|
|
|
|
// Remove the bubble if there is no more events
|
|
shouldRemoveBubbleData = (remainingEvents == 0);
|
|
}
|
|
}
|
|
|
|
// Check whether the bubble should be removed
|
|
if (shouldRemoveBubbleData)
|
|
{
|
|
[self removeCellData:bubbleData];
|
|
}
|
|
|
|
if (hasChanged)
|
|
{
|
|
// Update the delegate on main thread
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
if (self.delegate)
|
|
{
|
|
[self.delegate dataSource:self didCellChange:nil];
|
|
}
|
|
|
|
});
|
|
}
|
|
}
|
|
|
|
});
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)setFilterMessagesWithURL:(BOOL)filterMessagesWithURL
|
|
{
|
|
_filterMessagesWithURL = filterMessagesWithURL;
|
|
|
|
if (_isLive && _room)
|
|
{
|
|
// Update the event listeners by considering the right types for the live events.
|
|
[self refreshEventListeners:(_filterMessagesWithURL ? @[kMXEventTypeStringRoomMessage] : [MXKAppSettings standardAppSettings].allEventTypesForMessages)];
|
|
}
|
|
}
|
|
|
|
- (void)setEventFormatter:(MXKEventFormatter *)eventFormatter
|
|
{
|
|
if (_eventFormatter)
|
|
{
|
|
// Remove observers on previous event formatter settings
|
|
[_eventFormatter.settings removeObserver:self forKeyPath:@"showRedactionsInRoomHistory"];
|
|
[_eventFormatter.settings removeObserver:self forKeyPath:@"showUnsupportedEventsInRoomHistory"];
|
|
}
|
|
|
|
_eventFormatter = eventFormatter;
|
|
|
|
if (_eventFormatter)
|
|
{
|
|
// Add observer to flush stored data on settings changes
|
|
[_eventFormatter.settings addObserver:self forKeyPath:@"showRedactionsInRoomHistory" options:0 context:nil];
|
|
[_eventFormatter.settings addObserver:self forKeyPath:@"showUnsupportedEventsInRoomHistory" options:0 context:nil];
|
|
}
|
|
}
|
|
|
|
- (void)setShowBubblesDateTime:(BOOL)showBubblesDateTime
|
|
{
|
|
_showBubblesDateTime = showBubblesDateTime;
|
|
|
|
if (self.delegate)
|
|
{
|
|
// Reload all the table
|
|
[self.delegate dataSource:self didCellChange:nil];
|
|
}
|
|
}
|
|
|
|
- (void)setShowTypingNotifications:(BOOL)shouldShowTypingNotifications
|
|
{
|
|
_showTypingNotifications = shouldShowTypingNotifications;
|
|
|
|
if (shouldShowTypingNotifications)
|
|
{
|
|
// Register on typing notif
|
|
[self listenTypingNotifications];
|
|
}
|
|
else
|
|
{
|
|
// Remove the live listener
|
|
if (typingNotifListener)
|
|
{
|
|
[_timeline removeListener:typingNotifListener];
|
|
currentTypingUsers = nil;
|
|
typingNotifListener = nil;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)listenTypingNotifications
|
|
{
|
|
// Remove the previous live listener
|
|
if (typingNotifListener)
|
|
{
|
|
[_timeline removeListener:typingNotifListener];
|
|
currentTypingUsers = nil;
|
|
}
|
|
|
|
// Add typing notification listener
|
|
MXWeakify(self);
|
|
|
|
typingNotifListener = [_timeline listenToEventsOfTypes:@[kMXEventTypeStringTypingNotification] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState)
|
|
{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
// Handle only live events
|
|
if (direction == MXTimelineDirectionForwards)
|
|
{
|
|
// Retrieve typing users list
|
|
NSMutableArray *typingUsers = [NSMutableArray arrayWithArray:self.room.typingUsers];
|
|
|
|
// Remove typing info for the current user
|
|
NSUInteger index = [typingUsers indexOfObject:self.mxSession.myUser.userId];
|
|
if (index != NSNotFound)
|
|
{
|
|
[typingUsers removeObjectAtIndex:index];
|
|
}
|
|
// Ignore this notification if both arrays are empty
|
|
if (self->currentTypingUsers.count || typingUsers.count)
|
|
{
|
|
self->currentTypingUsers = typingUsers;
|
|
|
|
if (self.delegate)
|
|
{
|
|
// refresh all the table
|
|
[self.delegate dataSource:self didCellChange:nil];
|
|
}
|
|
}
|
|
}
|
|
}];
|
|
|
|
currentTypingUsers = _room.typingUsers;
|
|
}
|
|
|
|
- (void)cancelAllRequests
|
|
{
|
|
if (paginationRequest)
|
|
{
|
|
// We have to remove here the listener. A new pagination request may be triggered whereas the cancellation of this one is in progress
|
|
[_timeline removeListener:paginationListener];
|
|
paginationListener = nil;
|
|
|
|
[paginationRequest cancel];
|
|
paginationRequest = nil;
|
|
}
|
|
|
|
[super cancelAllRequests];
|
|
}
|
|
|
|
- (void)setDelegate:(id<MXKDataSourceDelegate>)delegate
|
|
{
|
|
super.delegate = delegate;
|
|
|
|
// Register to MXScanManager notification only when a delegate is set
|
|
if (delegate && self.mxSession.scanManager)
|
|
{
|
|
[self registerScanManagerNotifications];
|
|
}
|
|
|
|
// Register to reaction notification only when a delegate is set
|
|
if (delegate)
|
|
{
|
|
[self registerReactionsChangeListener];
|
|
[self registerEventEditsListener];
|
|
}
|
|
}
|
|
|
|
- (void)setRoom:(MXRoom *)room
|
|
{
|
|
if (![_room isEqual:room])
|
|
{
|
|
_room = room;
|
|
|
|
[self roomDidSet];
|
|
}
|
|
}
|
|
|
|
- (void)roomDidSet
|
|
{
|
|
|
|
}
|
|
|
|
- (BOOL)shouldQueueEventForProcessing:(MXEvent*)event roomState:(MXRoomState*)roomState direction:(MXTimelineDirection)direction
|
|
{
|
|
if (self.filterMessagesWithURL)
|
|
{
|
|
// Check whether the event has a value for the 'url' key in its content.
|
|
if (!event.getMediaURLs.count)
|
|
{
|
|
// ignore the event
|
|
return NO;
|
|
}
|
|
|
|
// Ignore voice message related to an actual voice broadcast.
|
|
if (event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil) {
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
// Check for undecryptable messages that were sent while the user was not in the room and hide them
|
|
if ([MXKAppSettings standardAppSettings].hidePreJoinedUndecryptableEvents
|
|
&& direction == MXTimelineDirectionBackwards)
|
|
{
|
|
[self checkForPreJoinUTDWithEvent:event roomState:roomState];
|
|
|
|
// Hide pre joint UTD events
|
|
if (self.shouldStopBackPagination)
|
|
{
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
if (!USE_THREAD_TIMELINE && direction == MXTimelineDirectionBackwards && self.threadId)
|
|
{
|
|
// when not using a thread timeline, data source will desperately fill the screen with events by filtering them locally.
|
|
// we can stop when we see the thread root event when paginating backwards
|
|
if ([event.eventId isEqualToString:self.threadId])
|
|
{
|
|
self.shouldStopBackPagination = YES;
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
#pragma mark - KVO
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
|
|
{
|
|
if ([@"showRedactionsInRoomHistory" isEqualToString:keyPath] || [@"showUnsupportedEventsInRoomHistory" isEqualToString:keyPath])
|
|
{
|
|
// Flush the current bubble data and rebuild them
|
|
[self reload];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Public methods
|
|
- (id<MXKRoomBubbleCellDataStoring>)cellDataAtIndex:(NSInteger)index
|
|
{
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData;
|
|
@synchronized(bubbles)
|
|
{
|
|
if (index < bubbles.count)
|
|
{
|
|
bubbleData = bubbles[index];
|
|
}
|
|
}
|
|
return bubbleData;
|
|
}
|
|
|
|
- (id<MXKRoomBubbleCellDataStoring>)cellDataOfEventWithEventId:(NSString *)eventId
|
|
{
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData;
|
|
@synchronized(eventIdToBubbleMap)
|
|
{
|
|
bubbleData = eventIdToBubbleMap[eventId];
|
|
}
|
|
return bubbleData;
|
|
}
|
|
|
|
- (NSInteger)indexOfCellDataWithEventId:(NSString *)eventId
|
|
{
|
|
NSInteger index = NSNotFound;
|
|
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData;
|
|
@synchronized(eventIdToBubbleMap)
|
|
{
|
|
bubbleData = eventIdToBubbleMap[eventId];
|
|
}
|
|
|
|
if (bubbleData)
|
|
{
|
|
@synchronized(bubbles)
|
|
{
|
|
index = [bubbles indexOfObject:bubbleData];
|
|
}
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
- (CGFloat)cellHeightAtIndex:(NSInteger)index withMaximumWidth:(CGFloat)maxWidth
|
|
{
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData = [self cellDataAtIndex:index];
|
|
|
|
// Sanity check
|
|
if (bubbleData && self.delegate)
|
|
{
|
|
// Compute here height of bubble cell
|
|
Class<MXKCellRendering> cellViewClass = [self.delegate cellViewClassForCellData:bubbleData];
|
|
return [cellViewClass heightForCellData:bubbleData withMaximumWidth:maxWidth];
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
- (void)invalidateBubblesCellDataCache
|
|
{
|
|
@synchronized(bubbles)
|
|
{
|
|
for (id<MXKRoomBubbleCellDataStoring> bubble in bubbles)
|
|
{
|
|
[bubble invalidateTextLayout];
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - Pagination
|
|
- (void)paginate:(NSUInteger)numItems direction:(MXTimelineDirection)direction onlyFromStore:(BOOL)onlyFromStore success:(void (^)(NSUInteger addedCellNumber))success failure:(void (^)(NSError *error))failure
|
|
{
|
|
// Check the current data source state, and the actual user membership for this room.
|
|
if (state != MXKDataSourceStateReady || ((self.room.summary.membership == MXMembershipUnknown || self.room.summary.membership == MXMembershipInvite) && ![self.roomState.historyVisibility isEqualToString:kMXRoomHistoryVisibilityWorldReadable]))
|
|
{
|
|
// Back pagination is not available here.
|
|
if (failure)
|
|
{
|
|
failure(nil);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (paginationRequest || secondaryPaginationRequest)
|
|
{
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] paginate: a pagination is already in progress", self);
|
|
if (failure)
|
|
{
|
|
failure(nil);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (NO == [self canPaginate:direction])
|
|
{
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] paginate: No more events to paginate", self);
|
|
if (success)
|
|
{
|
|
success(0);
|
|
}
|
|
}
|
|
|
|
__block NSUInteger addedCellNb = 0;
|
|
__block NSMutableArray<NSError*> *operationErrors = [NSMutableArray arrayWithCapacity:2];
|
|
dispatch_group_t dispatchGroup = dispatch_group_create();
|
|
|
|
// Define a new listener for this pagination
|
|
paginationListener = [_timeline listenToEventsOfTypes:(_filterMessagesWithURL ? @[kMXEventTypeStringRoomMessage] : [MXKAppSettings standardAppSettings].allEventTypesForMessages) onEvent:^(MXEvent *event, MXTimelineDirection direction2, MXRoomState *roomState) {
|
|
|
|
if (direction2 == direction)
|
|
{
|
|
[self queueEventForProcessing:event withRoomState:roomState direction:direction];
|
|
}
|
|
|
|
}];
|
|
|
|
// Keep a local reference to this listener.
|
|
id localPaginationListenerRef = paginationListener;
|
|
|
|
dispatch_group_enter(dispatchGroup);
|
|
// Launch the pagination
|
|
|
|
MXWeakify(self);
|
|
paginationRequest = [_timeline paginate:numItems
|
|
direction:direction
|
|
onlyFromStore:onlyFromStore
|
|
complete:^{
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
// Everything went well, remove the listener
|
|
self->paginationRequest = nil;
|
|
[self.timeline removeListener:self->paginationListener];
|
|
self->paginationListener = nil;
|
|
|
|
// Once done, process retrieved events
|
|
[self processQueuedEvents:^(NSUInteger addedHistoryCellNb, NSUInteger addedLiveCellNb) {
|
|
|
|
addedCellNb += (direction == MXTimelineDirectionBackwards) ? addedHistoryCellNb : addedLiveCellNb;
|
|
dispatch_group_leave(dispatchGroup);
|
|
|
|
}];
|
|
|
|
} failure:^(NSError *error) {
|
|
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] paginateBackMessages fails", self);
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
// Something wrong happened or the request was cancelled.
|
|
// Check whether the request is the actual one before removing listener and handling the retrieved events.
|
|
if (localPaginationListenerRef == self->paginationListener)
|
|
{
|
|
self->paginationRequest = nil;
|
|
[self.timeline removeListener:self->paginationListener];
|
|
self->paginationListener = nil;
|
|
|
|
// Process at least events retrieved from store
|
|
[self processQueuedEvents:^(NSUInteger addedHistoryCellNb, NSUInteger addedLiveCellNb) {
|
|
|
|
[operationErrors addObject:error];
|
|
if (addedHistoryCellNb)
|
|
{
|
|
addedCellNb += addedHistoryCellNb;
|
|
}
|
|
dispatch_group_leave(dispatchGroup);
|
|
|
|
}];
|
|
}
|
|
|
|
}];
|
|
|
|
if (_secondaryTimeline)
|
|
{
|
|
// Define a new listener for this pagination
|
|
secondaryPaginationListener = [_secondaryTimeline listenToEventsOfTypes:_secondaryRoomEventTypes onEvent:^(MXEvent *event, MXTimelineDirection direction2, MXRoomState *roomState) {
|
|
|
|
if (direction2 == direction)
|
|
{
|
|
[self queueEventForProcessing:event withRoomState:roomState direction:direction];
|
|
}
|
|
|
|
}];
|
|
|
|
// Keep a local reference to this listener.
|
|
id localPaginationListenerRef = secondaryPaginationListener;
|
|
|
|
dispatch_group_enter(dispatchGroup);
|
|
// Launch the pagination
|
|
MXWeakify(self);
|
|
secondaryPaginationRequest = [_secondaryTimeline paginate:numItems
|
|
direction:direction
|
|
onlyFromStore:onlyFromStore
|
|
complete:^{
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
// Everything went well, remove the listener
|
|
self->secondaryPaginationRequest = nil;
|
|
[self.secondaryTimeline removeListener:self->secondaryPaginationListener];
|
|
self->secondaryPaginationListener = nil;
|
|
|
|
// Once done, process retrieved events
|
|
[self processQueuedEvents:^(NSUInteger addedHistoryCellNb, NSUInteger addedLiveCellNb) {
|
|
|
|
addedCellNb += (direction == MXTimelineDirectionBackwards) ? addedHistoryCellNb : addedLiveCellNb;
|
|
dispatch_group_leave(dispatchGroup);
|
|
|
|
}];
|
|
|
|
} failure:^(NSError *error) {
|
|
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] paginateBackMessages fails", self);
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
// Something wrong happened or the request was cancelled.
|
|
// Check whether the request is the actual one before removing listener and handling the retrieved events.
|
|
if (localPaginationListenerRef == self->secondaryPaginationListener)
|
|
{
|
|
self->secondaryPaginationRequest = nil;
|
|
[self.secondaryTimeline removeListener:self->secondaryPaginationListener];
|
|
self->secondaryPaginationListener = nil;
|
|
|
|
// Process at least events retrieved from store
|
|
[self processQueuedEvents:^(NSUInteger addedHistoryCellNb, NSUInteger addedLiveCellNb) {
|
|
|
|
[operationErrors addObject:error];
|
|
if (addedHistoryCellNb)
|
|
{
|
|
addedCellNb += addedHistoryCellNb;
|
|
}
|
|
dispatch_group_leave(dispatchGroup);
|
|
|
|
}];
|
|
}
|
|
|
|
}];
|
|
}
|
|
|
|
dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{
|
|
if (operationErrors.count)
|
|
{
|
|
if (failure)
|
|
{
|
|
failure(operationErrors.firstObject);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (success)
|
|
{
|
|
success(addedCellNb);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)paginateToFillRect:(CGRect)rect direction:(MXTimelineDirection)direction withMinRequestMessagesCount:(NSUInteger)minRequestMessagesCount success:(void (^)(void))success failure:(void (^)(NSError *error))failure
|
|
{
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] paginateToFillRect: %@", self, NSStringFromCGRect(rect));
|
|
|
|
// During the first call of this method, the delegate is supposed defined.
|
|
// This delegate may be removed whereas this method is called by itself after a pagination request.
|
|
// The delegate is required here to be able to compute cell height (and prevent infinite loop in case of reentrancy).
|
|
if (!self.delegate)
|
|
{
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] paginateToFillRect ignored (delegate is undefined)", self);
|
|
if (failure)
|
|
{
|
|
failure(nil);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Get the total height of cells already loaded in memory
|
|
CGFloat minMessageHeight = CGFLOAT_MAX;
|
|
CGFloat bubblesTotalHeight = 0;
|
|
|
|
@synchronized(bubbles)
|
|
{
|
|
// Check whether data has been aldready loaded
|
|
if (bubbles.count)
|
|
{
|
|
NSUInteger eventsCount = 0;
|
|
for (NSInteger i = bubbles.count - 1; i >= 0; i--)
|
|
{
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData = bubbles[i];
|
|
eventsCount += bubbleData.events.count;
|
|
|
|
CGFloat bubbleHeight = [self cellHeightAtIndex:i withMaximumWidth:rect.size.width];
|
|
// Sanity check
|
|
if (bubbleHeight)
|
|
{
|
|
bubblesTotalHeight += bubbleHeight;
|
|
|
|
if (bubblesTotalHeight > rect.size.height)
|
|
{
|
|
// No need to compute more cells heights, there are enough to fill the rect
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] -> %tu already loaded bubbles (%tu events) are enough to fill the screen", self, bubbles.count - i, eventsCount);
|
|
break;
|
|
}
|
|
|
|
// Compute the minimal height an event takes
|
|
minMessageHeight = MIN(minMessageHeight, bubbleHeight / bubbleData.events.count);
|
|
}
|
|
}
|
|
}
|
|
else if (minRequestMessagesCount && [self canPaginate:direction])
|
|
{
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] paginateToFillRect: Prefill with data from the store", self);
|
|
// Give a chance to load data from the store before doing homeserver requests
|
|
// Reuse minRequestMessagesCount because we need to provide a number.
|
|
[self paginate:minRequestMessagesCount direction:direction onlyFromStore:YES success:^(NSUInteger addedCellNumber) {
|
|
|
|
// Then retry
|
|
[self paginateToFillRect:rect direction:direction withMinRequestMessagesCount:minRequestMessagesCount success:success failure:failure];
|
|
|
|
} failure:failure];
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Is there enough cells to cover all the requested height?
|
|
if (bubblesTotalHeight < rect.size.height)
|
|
{
|
|
// No. Paginate to get more messages
|
|
if ([self canPaginate:direction])
|
|
{
|
|
// Bound the minimal height to 44
|
|
minMessageHeight = MIN(minMessageHeight, 44);
|
|
|
|
// Load messages to cover the remaining height
|
|
// Use an extra of 50% to manage unsupported/unexpected/redated events
|
|
NSUInteger messagesToLoad = ceil((rect.size.height - bubblesTotalHeight) / minMessageHeight * 1.5);
|
|
|
|
// It does not worth to make a pagination request for only 1 message.
|
|
// So, use minRequestMessagesCount
|
|
messagesToLoad = MAX(messagesToLoad, minRequestMessagesCount);
|
|
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] paginateToFillRect: need to paginate %tu events to cover %fpx", self, messagesToLoad, rect.size.height - bubblesTotalHeight);
|
|
[self paginate:messagesToLoad direction:direction onlyFromStore:NO success:^(NSUInteger addedCellNumber) {
|
|
|
|
[self paginateToFillRect:rect direction:direction withMinRequestMessagesCount:minRequestMessagesCount success:success failure:failure];
|
|
|
|
} failure:failure];
|
|
}
|
|
else
|
|
{
|
|
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] paginateToFillRect: No more events to paginate", self);
|
|
if (success)
|
|
{
|
|
success();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Yes. Nothing to do
|
|
if (success)
|
|
{
|
|
success();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#pragma mark - Sending
|
|
- (void)sendTextMessage:(NSString *)text success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure
|
|
{
|
|
__block MXEvent *localEchoEvent = nil;
|
|
|
|
BOOL isEmote = [self isMessageAnEmote:text];
|
|
NSString *sanitizedText = [self sanitizedMessageText:text];
|
|
NSString *html = [self htmlMessageFromSanitizedText:sanitizedText];
|
|
|
|
// Make the request to the homeserver
|
|
if (isEmote)
|
|
{
|
|
[_room sendEmote:sanitizedText formattedText:html threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure];
|
|
}
|
|
else
|
|
{
|
|
[_room sendTextMessage:sanitizedText formattedText:html threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure];
|
|
}
|
|
|
|
if (localEchoEvent)
|
|
{
|
|
// Make the data source digest this fake local echo message
|
|
[self queueEventForProcessing:localEchoEvent withRoomState:self.roomState direction:MXTimelineDirectionForwards];
|
|
[self processQueuedEvents:nil];
|
|
}
|
|
}
|
|
|
|
- (void)sendReplyToEvent:(MXEvent*)eventToReply
|
|
withTextMessage:(NSString *)text
|
|
success:(void (^)(NSString *))success
|
|
failure:(void (^)(NSError *))failure
|
|
{
|
|
__block MXEvent *localEchoEvent = nil;
|
|
|
|
NSString *sanitizedText = [self sanitizedMessageText:text];
|
|
NSString *html = [self htmlMessageFromSanitizedText:sanitizedText];
|
|
|
|
id<MXSendReplyEventStringLocalizerProtocol> stringLocalizer = [MXKSendReplyEventStringLocalizer new];
|
|
|
|
[_room sendReplyToEvent:eventToReply withTextMessage:sanitizedText formattedTextMessage:html stringLocalizer:stringLocalizer threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure];
|
|
|
|
if (localEchoEvent)
|
|
{
|
|
// Make the data source digest this fake local echo message
|
|
[self queueEventForProcessing:localEchoEvent withRoomState:self.roomState direction:MXTimelineDirectionForwards];
|
|
[self processQueuedEvents:nil];
|
|
}
|
|
}
|
|
|
|
- (BOOL)isMessageAnEmote:(NSString*)text
|
|
{
|
|
return [text hasPrefix:emoteMessageSlashCommandPrefix];
|
|
}
|
|
|
|
- (NSString*)sanitizedMessageText:(NSString*)rawText
|
|
{
|
|
NSString *text;
|
|
|
|
//Remove NULL bytes from the string, as they are likely to trip up many things later,
|
|
//including our own C-based Markdown-to-HTML convertor.
|
|
//
|
|
//Normally, we don't expect people to be entering NULL bytes in messages,
|
|
//but because of a bug in iOS 11, it's easy to have it happen.
|
|
//
|
|
//iOS 11's Smart Punctuation feature "conveniently" converts double hyphens (`--`) to longer en-dashes (`—`).
|
|
//However, when adding any kind of dash/hyphen after such an en-dash,
|
|
//iOS would also insert a NULL byte inbetween the dashes (`<en-dash>NULL<some other dash>`).
|
|
//
|
|
//Even if a future iOS update fixes this,
|
|
//we'd better be defensive and always remove occurrences of NULL bytes from text messages.
|
|
text = [rawText stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"%C", 0x00000000] withString:@""];
|
|
|
|
// Check whether the message is an emote
|
|
if ([self isMessageAnEmote:text])
|
|
{
|
|
// Remove "/me " string
|
|
text = [text substringFromIndex:emoteMessageSlashCommandPrefix.length];
|
|
}
|
|
|
|
return text;
|
|
}
|
|
|
|
- (NSString*)htmlMessageFromSanitizedText:(NSString*)sanitizedText
|
|
{
|
|
NSString *html;
|
|
|
|
// Did user use Markdown text?
|
|
NSString *htmlStringFromMarkdown = [_eventFormatter htmlStringFromMarkdownString:sanitizedText];
|
|
|
|
if ([htmlStringFromMarkdown isEqualToString:sanitizedText])
|
|
{
|
|
// No formatted string
|
|
html = nil;
|
|
}
|
|
else
|
|
{
|
|
html = htmlStringFromMarkdown;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
- (void)sendImage:(UIImage *)image success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure
|
|
{
|
|
// Make sure the uploaded image orientation is up
|
|
image = [MXKTools forceImageOrientationUp:image];
|
|
|
|
// Only jpeg image is supported here
|
|
NSString *mimetype = @"image/jpeg";
|
|
NSData *imageData = UIImageJPEGRepresentation(image, 0.9);
|
|
|
|
// Shall we need to consider a thumbnail?
|
|
UIImage *thumbnail = nil;
|
|
if (_room.summary.isEncrypted)
|
|
{
|
|
// Thumbnail is useful only in case of encrypted room
|
|
thumbnail = [MXKTools reduceImage:image toFitInSize:CGSizeMake(800, 600)];
|
|
if (thumbnail == image)
|
|
{
|
|
thumbnail = nil;
|
|
}
|
|
}
|
|
|
|
[self sendImageData:imageData withImageSize:image.size mimeType:mimetype andThumbnail:thumbnail success:success failure:failure];
|
|
}
|
|
|
|
- (BOOL)canReplyToEventWithId:(NSString*)eventIdToReply
|
|
{
|
|
MXEvent *eventToReply = [self eventWithEventId:eventIdToReply];
|
|
return [self.room canReplyToEvent:eventToReply];
|
|
}
|
|
|
|
- (void)sendImage:(NSData *)imageData mimeType:(NSString *)mimetype success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure
|
|
{
|
|
UIImage *image = [UIImage imageWithData:imageData];
|
|
|
|
// Shall we need to consider a thumbnail?
|
|
UIImage *thumbnail = nil;
|
|
if (_room.summary.isEncrypted)
|
|
{
|
|
// Thumbnail is useful only in case of encrypted room
|
|
thumbnail = [MXKTools reduceImage:image toFitInSize:CGSizeMake(800, 600)];
|
|
if (thumbnail == image)
|
|
{
|
|
thumbnail = nil;
|
|
}
|
|
}
|
|
|
|
[self sendImageData:imageData withImageSize:image.size mimeType:mimetype andThumbnail:thumbnail success:success failure:failure];
|
|
}
|
|
|
|
- (void)sendImageData:(NSData*)imageData withImageSize:(CGSize)imageSize mimeType:(NSString*)mimetype andThumbnail:(UIImage*)thumbnail success:(void (^)(NSString *eventId))success failure:(void (^)(NSError *error))failure
|
|
{
|
|
__block MXEvent *localEchoEvent = nil;
|
|
|
|
[_room sendImage:imageData withImageSize:imageSize mimeType:mimetype andThumbnail:thumbnail threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure];
|
|
|
|
if (localEchoEvent)
|
|
{
|
|
// Make the data source digest this fake local echo message
|
|
[self queueEventForProcessing:localEchoEvent withRoomState:self.roomState direction:MXTimelineDirectionForwards];
|
|
[self processQueuedEvents:nil];
|
|
}
|
|
}
|
|
|
|
- (void)sendVideo:(NSURL *)videoLocalURL withThumbnail:(UIImage *)videoThumbnail success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure
|
|
{
|
|
AVURLAsset *videoAsset = [AVURLAsset assetWithURL:videoLocalURL];
|
|
[self sendVideoAsset:videoAsset withThumbnail:videoThumbnail success:success failure:failure];
|
|
}
|
|
|
|
- (void)sendVideoAsset:(AVAsset *)videoAsset withThumbnail:(UIImage *)videoThumbnail success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure
|
|
{
|
|
__block MXEvent *localEchoEvent = nil;
|
|
|
|
[_room sendVideoAsset:videoAsset withThumbnail:videoThumbnail threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure];
|
|
|
|
if (localEchoEvent)
|
|
{
|
|
// Make the data source digest this fake local echo message
|
|
[self queueEventForProcessing:localEchoEvent withRoomState:self.roomState direction:MXTimelineDirectionForwards];
|
|
[self processQueuedEvents:nil];
|
|
}
|
|
}
|
|
|
|
- (void)sendAudioFile:(NSURL *)audioFileLocalURL mimeType:mimeType success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure
|
|
{
|
|
__block MXEvent *localEchoEvent = nil;
|
|
|
|
[_room sendAudioFile:audioFileLocalURL mimeType:mimeType threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure keepActualFilename:YES];
|
|
|
|
if (localEchoEvent)
|
|
{
|
|
// Make the data source digest this fake local echo message
|
|
[self queueEventForProcessing:localEchoEvent withRoomState:self.roomState direction:MXTimelineDirectionForwards];
|
|
[self processQueuedEvents:nil];
|
|
}
|
|
}
|
|
|
|
- (void)sendVoiceMessage:(NSURL *)audioFileLocalURL
|
|
additionalContentParams:(NSDictionary *)additionalContentParams
|
|
mimeType:mimeType
|
|
duration:(NSUInteger)duration
|
|
samples:(NSArray<NSNumber *> *)samples
|
|
success:(void (^)(NSString *))success
|
|
failure:(void (^)(NSError *))failure
|
|
{
|
|
__block MXEvent *localEchoEvent = nil;
|
|
|
|
[_room sendVoiceMessage:audioFileLocalURL additionalContentParams:additionalContentParams mimeType:mimeType duration:duration samples:samples threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure keepActualFilename:YES];
|
|
|
|
if (localEchoEvent)
|
|
{
|
|
// Make the data source digest this fake local echo message
|
|
[self queueEventForProcessing:localEchoEvent withRoomState:self.roomState direction:MXTimelineDirectionForwards];
|
|
[self processQueuedEvents:nil];
|
|
}
|
|
}
|
|
|
|
|
|
- (void)sendFile:(NSURL *)fileLocalURL mimeType:(NSString*)mimeType success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure
|
|
{
|
|
__block MXEvent *localEchoEvent = nil;
|
|
|
|
[_room sendFile:fileLocalURL mimeType:mimeType threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure];
|
|
|
|
if (localEchoEvent)
|
|
{
|
|
// Make the data source digest this fake local echo message
|
|
[self queueEventForProcessing:localEchoEvent withRoomState:self.roomState direction:MXTimelineDirectionForwards];
|
|
[self processQueuedEvents:nil];
|
|
}
|
|
}
|
|
|
|
- (void)sendMessageWithContent:(NSDictionary *)msgContent success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure
|
|
{
|
|
__block MXEvent *localEchoEvent = nil;
|
|
|
|
// Make the request to the homeserver
|
|
[_room sendMessageWithContent:msgContent threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure];
|
|
|
|
if (localEchoEvent)
|
|
{
|
|
// Make the data source digest this fake local echo message
|
|
[self queueEventForProcessing:localEchoEvent withRoomState:self.roomState direction:MXTimelineDirectionForwards];
|
|
[self processQueuedEvents:nil];
|
|
}
|
|
}
|
|
|
|
- (void)sendLocationWithLatitude:(double)latitude
|
|
longitude:(double)longitude
|
|
description:(NSString *)description
|
|
coordinateType:(MXEventAssetType)coordinateType
|
|
success:(void (^)(NSString *))success
|
|
failure:(void (^)(NSError *))failure
|
|
{
|
|
__block MXEvent *localEchoEvent = nil;
|
|
|
|
// Make the request to the homeserver
|
|
[_room sendLocationWithLatitude:latitude
|
|
longitude:longitude
|
|
description:description
|
|
threadId:self.threadId
|
|
localEcho:&localEchoEvent
|
|
assetType:coordinateType
|
|
success:success failure:failure];
|
|
|
|
if (localEchoEvent)
|
|
{
|
|
// Make the data source digest this fake local echo message
|
|
[self queueEventForProcessing:localEchoEvent withRoomState:self.roomState direction:MXTimelineDirectionForwards];
|
|
[self processQueuedEvents:nil];
|
|
}
|
|
}
|
|
|
|
- (void)sendEventOfType:(MXEventTypeString)eventTypeString content:(NSDictionary<NSString*, id>*)msgContent success:(void (^)(NSString *eventId))success failure:(void (^)(NSError *error))failure
|
|
{
|
|
__block MXEvent *localEchoEvent = nil;
|
|
|
|
// Make the request to the homeserver
|
|
[_room sendEventOfType:eventTypeString content:msgContent threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure];
|
|
|
|
if (localEchoEvent)
|
|
{
|
|
// Make the data source digest this fake local echo message
|
|
[self queueEventForProcessing:localEchoEvent withRoomState:self.roomState direction:MXTimelineDirectionForwards];
|
|
[self processQueuedEvents:nil];
|
|
}
|
|
}
|
|
|
|
- (void)resendEventWithEventId:(NSString *)eventId success:(void (^)(NSString *))success failure:(void (^)(NSError *))failure
|
|
{
|
|
MXEvent *event = [self eventWithEventId:eventId];
|
|
|
|
// Sanity check
|
|
if (!event)
|
|
{
|
|
return;
|
|
}
|
|
|
|
MXLogInfo(@"[MXKRoomDataSource][%p] resendEventWithEventId. EventId: %@", self, event.eventId);
|
|
|
|
// Check first whether the event is encrypted
|
|
if ([event.wireType isEqualToString:kMXEventTypeStringRoomEncrypted])
|
|
{
|
|
// We try here to resent an encrypted event
|
|
// Note: we keep the existing local echo.
|
|
[_room sendEventOfType:kMXEventTypeStringRoomEncrypted content:event.wireContent threadId:self.threadId localEcho:&event success:success failure:failure];
|
|
}
|
|
else if ([event.type isEqualToString:kMXEventTypeStringRoomMessage])
|
|
{
|
|
// And retry the send the message according to its type
|
|
NSString *msgType = event.content[kMXMessageTypeKey];
|
|
if ([msgType isEqualToString:kMXMessageTypeText] || [msgType isEqualToString:kMXMessageTypeEmote])
|
|
{
|
|
// Resend the Matrix event by reusing the existing echo
|
|
[_room sendMessageWithContent:event.content threadId:self.threadId localEcho:&event success:success failure:failure];
|
|
}
|
|
else if ([msgType isEqualToString:kMXMessageTypeImage])
|
|
{
|
|
// Check whether the sending failed while uploading the data.
|
|
// If the content url corresponds to a upload id, the upload was not complete.
|
|
NSString *contentURL = event.content[@"url"];
|
|
if (contentURL && [contentURL hasPrefix:kMXMediaUploadIdPrefix])
|
|
{
|
|
NSString *mimetype = nil;
|
|
if (event.content[@"info"])
|
|
{
|
|
mimetype = event.content[@"info"][@"mimetype"];
|
|
}
|
|
|
|
NSString *localImagePath = [MXMediaManager cachePathForMatrixContentURI:contentURL andType:mimetype inFolder:_roomId];
|
|
UIImage* image = [MXMediaManager loadPictureFromFilePath:localImagePath];
|
|
if (image)
|
|
{
|
|
// Restart sending the image from the beginning.
|
|
|
|
// Remove the local echo.
|
|
[self removeEventWithEventId:eventId];
|
|
|
|
if (mimetype)
|
|
{
|
|
NSData *imageData = [NSData dataWithContentsOfFile:localImagePath];
|
|
[self sendImage:imageData mimeType:mimetype success:success failure:failure];
|
|
}
|
|
else
|
|
{
|
|
[self sendImage:image success:success failure:failure];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (failure)
|
|
{
|
|
failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendGeneric userInfo:nil]);
|
|
}
|
|
MXLogWarning(@"[MXKRoomDataSource][%p] resendEventWithEventId: Warning - Unable to resend room message of type: %@", self, msgType);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Resend the Matrix event by reusing the existing echo
|
|
[_room sendMessageWithContent:event.content threadId:self.threadId localEcho:&event success:success failure:failure];
|
|
}
|
|
}
|
|
else if ([msgType isEqualToString:kMXMessageTypeAudio])
|
|
{
|
|
// Check whether the sending failed while uploading the data.
|
|
// If the content url corresponds to a upload id, the upload was not complete.
|
|
NSString *contentURL = event.content[@"url"];
|
|
if (!contentURL || ![contentURL hasPrefix:kMXMediaUploadIdPrefix])
|
|
{
|
|
// Resend the Matrix event by reusing the existing echo
|
|
[_room sendMessageWithContent:event.content threadId:self.threadId localEcho:&event success:success failure:failure];
|
|
return;
|
|
}
|
|
|
|
NSString *mimetype = event.content[@"info"][@"mimetype"];
|
|
NSString *localFilePath = [MXMediaManager cachePathForMatrixContentURI:contentURL andType:mimetype inFolder:_roomId];
|
|
NSURL *localFileURL = [NSURL URLWithString:localFilePath];
|
|
|
|
if (![NSFileManager.defaultManager fileExistsAtPath:localFilePath]) {
|
|
if (failure)
|
|
{
|
|
failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendInvalidLocalFilePath userInfo:nil]);
|
|
}
|
|
MXLogWarning(@"[MXKRoomDataSource][%p] resendEventWithEventId: Warning - Unable to resend voice message, invalid file path.", self);
|
|
return;
|
|
}
|
|
|
|
// Remove the local echo.
|
|
[self removeEventWithEventId:eventId];
|
|
|
|
if (event.isVoiceMessage) {
|
|
// Voice message
|
|
NSNumber *duration = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioDuration];
|
|
NSArray<NSNumber *> *samples = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioWaveform];
|
|
|
|
// Additional content params in case it is a voicebroacast chunk
|
|
NSDictionary* additionalContentParams = nil;
|
|
if (event.content[kMXEventRelationRelatesToKey] != nil && event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType] != nil) {
|
|
additionalContentParams = @{
|
|
kMXEventRelationRelatesToKey: event.content[kMXEventRelationRelatesToKey],
|
|
VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType: event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType]
|
|
};
|
|
}
|
|
|
|
[self sendVoiceMessage:localFileURL additionalContentParams:additionalContentParams mimeType:mimetype duration:duration.doubleValue samples:samples success:success failure:failure];
|
|
} else {
|
|
[self sendAudioFile:localFileURL mimeType:mimetype success:success failure:failure];
|
|
}
|
|
}
|
|
else if ([msgType isEqualToString:kMXMessageTypeVideo])
|
|
{
|
|
// Check whether the sending failed while uploading the data.
|
|
// If the content url corresponds to a upload id, the upload was not complete.
|
|
NSString *contentURL = event.content[@"url"];
|
|
if (contentURL && [contentURL hasPrefix:kMXMediaUploadIdPrefix])
|
|
{
|
|
// TODO: Support resend on attached video when upload has been failed.
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] resendEventWithEventId: Warning - Unable to resend attached video (upload was not complete)", self);
|
|
failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendInvalidMessageType userInfo:nil]);
|
|
}
|
|
else
|
|
{
|
|
// Resend the Matrix event by reusing the existing echo
|
|
[_room sendMessageWithContent:event.content threadId:self.threadId localEcho:&event success:success failure:failure];
|
|
}
|
|
}
|
|
else if ([msgType isEqualToString:kMXMessageTypeFile])
|
|
{
|
|
// Check whether the sending failed while uploading the data.
|
|
// If the content url corresponds to a upload id, the upload was not complete.
|
|
NSString *contentURL = event.content[@"url"];
|
|
if (contentURL && [contentURL hasPrefix:kMXMediaUploadIdPrefix])
|
|
{
|
|
NSString *mimetype = nil;
|
|
if (event.content[@"info"])
|
|
{
|
|
mimetype = event.content[@"info"][@"mimetype"];
|
|
}
|
|
|
|
if (mimetype)
|
|
{
|
|
// Restart sending the image from the beginning.
|
|
|
|
// Remove the local echo
|
|
[self removeEventWithEventId:eventId];
|
|
|
|
NSString *localFilePath = [MXMediaManager cachePathForMatrixContentURI:contentURL andType:mimetype inFolder:_roomId];
|
|
|
|
[self sendFile:[NSURL fileURLWithPath:localFilePath isDirectory:NO] mimeType:mimetype success:success failure:failure];
|
|
}
|
|
else
|
|
{
|
|
if (failure)
|
|
{
|
|
failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendGeneric userInfo:nil]);
|
|
}
|
|
MXLogWarning(@"[MXKRoomDataSource][%p] resendEventWithEventId: Warning - Unable to resend room message of type: %@", self, msgType);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Resend the Matrix event by reusing the existing echo
|
|
[_room sendMessageWithContent:event.content threadId:self.threadId localEcho:&event success:success failure:failure];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (failure)
|
|
{
|
|
failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendInvalidMessageType userInfo:nil]);
|
|
}
|
|
MXLogWarning(@"[MXKRoomDataSource][%p] resendEventWithEventId: Warning - Unable to resend room message of type: %@", self, msgType);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (failure)
|
|
{
|
|
failure([NSError errorWithDomain:MXKRoomDataSourceErrorDomain code:MXKRoomDataSourceErrorResendInvalidMessageType userInfo:nil]);
|
|
}
|
|
MXLogWarning(@"[MXKRoomDataSource][%p] MXKRoomDataSource: Warning - Only resend of MXEventTypeRoomMessage is allowed. Event.type: %@", self, event.type);
|
|
}
|
|
}
|
|
|
|
|
|
#pragma mark - Events management
|
|
- (MXEvent *)eventWithEventId:(NSString *)eventId
|
|
{
|
|
MXEvent *theEvent;
|
|
|
|
// First, retrieve the cell data hosting the event
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData = [self cellDataOfEventWithEventId:eventId];
|
|
if (bubbleData)
|
|
{
|
|
// Then look into the events in this cell
|
|
for (MXEvent *event in bubbleData.events)
|
|
{
|
|
if ([event.eventId isEqualToString:eventId])
|
|
{
|
|
theEvent = event;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return theEvent;
|
|
}
|
|
|
|
- (void)removeEventWithEventId:(NSString *)eventId
|
|
{
|
|
MXLogVerbose(@"[MXKRoomDataSource][%p] removeEventWithEventId: %@", self, eventId);
|
|
|
|
// First, retrieve the cell data hosting the event
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData = [self cellDataOfEventWithEventId:eventId];
|
|
if (bubbleData)
|
|
{
|
|
NSUInteger remainingEvents;
|
|
@synchronized (bubbleData)
|
|
{
|
|
remainingEvents = [bubbleData removeEvent:eventId];
|
|
}
|
|
|
|
// If there is no more events in the bubble, remove it
|
|
if (0 == remainingEvents)
|
|
{
|
|
[self removeCellData:bubbleData];
|
|
}
|
|
|
|
// Remove the event from the outgoing messages storage
|
|
[_room removeOutgoingMessage:eventId];
|
|
|
|
// Update the delegate
|
|
if (self.delegate)
|
|
{
|
|
[self.delegate dataSource:self didCellChange:nil];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)didReceiveReceiptEvent:(MXEvent *)receiptEvent roomState:(MXRoomState *)roomState
|
|
{
|
|
// Do the processing on the same processing queue
|
|
MXWeakify(self);
|
|
dispatch_async(MXKRoomDataSource.processingQueue, ^{
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
// Remove the previous displayed read receipt for each user who sent a
|
|
// new read receipt.
|
|
// To implement it, we need to find the sender id of each new read receipt
|
|
// among the read receipts array of all events in all bubbles.
|
|
NSArray *readReceiptSenders = receiptEvent.readReceiptSenders;
|
|
|
|
@synchronized(self->bubbles)
|
|
{
|
|
for (MXKRoomBubbleCellData *cellData in self->bubbles)
|
|
{
|
|
NSMutableDictionary<NSString* /* eventId */, NSArray<MXReceiptData*> *> *updatedCellDataReadReceipts = [NSMutableDictionary dictionary];
|
|
|
|
NSDictionary<NSString*, NSArray<MXReceiptData*>*> *readReceiptsCopy = [cellData.readReceipts mutableDeepCopy];
|
|
for (NSString *eventId in readReceiptsCopy)
|
|
{
|
|
for (MXReceiptData *receiptData in readReceiptsCopy[eventId])
|
|
{
|
|
for (NSString *senderId in readReceiptSenders)
|
|
{
|
|
if ([receiptData.userId isEqualToString:senderId])
|
|
{
|
|
if (!updatedCellDataReadReceipts[eventId])
|
|
{
|
|
updatedCellDataReadReceipts[eventId] = readReceiptsCopy[eventId];
|
|
}
|
|
|
|
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"userId!=%@", receiptData.userId];
|
|
updatedCellDataReadReceipts[eventId] = [updatedCellDataReadReceipts[eventId] filteredArrayUsingPredicate:predicate];
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// Flush found changed to the cell data
|
|
for (NSString *eventId in updatedCellDataReadReceipts)
|
|
{
|
|
if (updatedCellDataReadReceipts[eventId].count)
|
|
{
|
|
[self updateCellData:cellData withReadReceipts:updatedCellDataReadReceipts[eventId] forEventId:eventId];
|
|
}
|
|
else
|
|
{
|
|
[self updateCellData:cellData withReadReceipts:nil forEventId:eventId];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
dispatch_group_t dispatchGroup = dispatch_group_create();
|
|
|
|
// Update cell data we have received a read receipt for
|
|
NSArray *readEventIds = receiptEvent.readReceiptEventIds;
|
|
if (RiotSettings.shared.enableThreads)
|
|
{
|
|
NSArray *readThreadIds = receiptEvent.readReceiptThreadIds;
|
|
for (int i = 0 ; i < readEventIds.count ; i++)
|
|
{
|
|
NSString *eventId = readEventIds[i];
|
|
MXKRoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:eventId];
|
|
if (cellData)
|
|
{
|
|
if ([readThreadIds[i] isEqualToString:kMXEventUnthreaded])
|
|
{
|
|
// Unthreaded RR must be propagated through all threads.
|
|
[self.mxSession.threadingService allThreadsInRoomWithId:self.roomId onlyParticipated:NO completion:^(NSArray<id<MXThreadProtocol>> *threads) {
|
|
NSMutableArray *threadIds = [NSMutableArray arrayWithObject:kMXEventTimelineMain];
|
|
for (id<MXThreadProtocol> thread in threads)
|
|
{
|
|
[threadIds addObject:thread.id];
|
|
}
|
|
|
|
for (NSString *threadId in threadIds)
|
|
{
|
|
@synchronized(self->bubbles)
|
|
{
|
|
dispatch_group_enter(dispatchGroup);
|
|
[self addReadReceiptsForEvent:eventId threadId:threadId inCellDatas:self->bubbles startingAtCellData:cellData completion:^{
|
|
dispatch_group_leave(dispatchGroup);
|
|
}];
|
|
}
|
|
}
|
|
}];
|
|
}
|
|
else
|
|
{
|
|
NSString *threadId = readThreadIds[i];
|
|
@synchronized(self->bubbles)
|
|
{
|
|
dispatch_group_enter(dispatchGroup);
|
|
[self addReadReceiptsForEvent:eventId threadId:threadId inCellDatas:self->bubbles startingAtCellData:cellData completion:^{
|
|
dispatch_group_leave(dispatchGroup);
|
|
}];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If
|
|
for (NSString *eventId in readEventIds)
|
|
{
|
|
MXKRoomBubbleCellData *cellData = [self cellDataOfEventWithEventId:eventId];
|
|
@synchronized(self->bubbles)
|
|
{
|
|
dispatch_group_enter(dispatchGroup);
|
|
[self addReadReceiptsForEvent:eventId threadId:kMXEventTimelineMain inCellDatas:self->bubbles startingAtCellData:cellData completion:^{
|
|
dispatch_group_leave(dispatchGroup);
|
|
}];
|
|
}
|
|
}
|
|
}
|
|
|
|
dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{
|
|
if (self.delegate)
|
|
{
|
|
[self.delegate dataSource:self didCellChange:nil];
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
- (void)updateCellData:(MXKRoomBubbleCellData*)cellData withReadReceipts:(NSArray<MXReceiptData*>*)readReceipts forEventId:(NSString*)eventId
|
|
{
|
|
cellData.readReceipts[eventId] = readReceipts;
|
|
|
|
// Indicate that the text message layout should be recomputed.
|
|
[cellData invalidateTextLayout];
|
|
}
|
|
|
|
- (void)handleUnsentMessages
|
|
{
|
|
// Add the unsent messages at the end of the conversation
|
|
NSArray<MXEvent*>* outgoingMessages = _room.outgoingMessages;
|
|
|
|
[self.mxSession decryptEvents:outgoingMessages inTimeline:nil onComplete:^(NSArray<MXEvent *> *failedEvents) {
|
|
|
|
for (MXEvent *outgoingMessage in outgoingMessages)
|
|
{
|
|
[self queueEventForProcessing:outgoingMessage withRoomState:self.roomState direction:MXTimelineDirectionForwards];
|
|
}
|
|
|
|
MXLogVerbose(@"[MXKRoomDataSource][%p] handleUnsentMessages: queued %tu events", self, outgoingMessages.count);
|
|
|
|
[self processQueuedEvents:nil];
|
|
}];
|
|
}
|
|
|
|
#pragma mark - Bubble collapsing
|
|
|
|
- (void)collapseRoomBubble:(id<MXKRoomBubbleCellDataStoring>)bubbleData collapsed:(BOOL)collapsed
|
|
{
|
|
if (bubbleData.collapsed != collapsed)
|
|
{
|
|
id<MXKRoomBubbleCellDataStoring> nextBubbleData = bubbleData;
|
|
do
|
|
{
|
|
nextBubbleData.collapsed = collapsed;
|
|
}
|
|
while ((nextBubbleData = nextBubbleData.nextCollapsableCellData));
|
|
|
|
if (self.delegate)
|
|
{
|
|
// Reload all the table
|
|
[self.delegate dataSource:self didCellChange:nil];
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - Private methods
|
|
|
|
- (void)replaceEvent:(MXEvent*)eventToReplace withEvent:(MXEvent*)event
|
|
{
|
|
MXLogVerbose(@"[MXKRoomDataSource][%p] replaceEvent: %@ with: %@", self, eventToReplace.eventId, event.eventId);
|
|
|
|
if (eventToReplace.isLocalEvent)
|
|
{
|
|
// Stop listening to the identifier change for the replaced event.
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidChangeIdentifierNotification object:eventToReplace];
|
|
}
|
|
|
|
// Retrieve the cell data hosting the replaced event
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData = [self cellDataOfEventWithEventId:eventToReplace.eventId];
|
|
if (!bubbleData)
|
|
{
|
|
return;
|
|
}
|
|
|
|
NSUInteger remainingEvents;
|
|
@synchronized (bubbleData)
|
|
{
|
|
// Check whether the local echo is replaced or removed
|
|
if (event)
|
|
{
|
|
remainingEvents = [bubbleData updateEvent:eventToReplace.eventId withEvent:event];
|
|
}
|
|
else
|
|
{
|
|
remainingEvents = [bubbleData removeEvent:eventToReplace.eventId];
|
|
}
|
|
}
|
|
|
|
// Update bubbles mapping
|
|
@synchronized (eventIdToBubbleMap)
|
|
{
|
|
// Remove the broken link from the map
|
|
[eventIdToBubbleMap removeObjectForKey:eventToReplace.eventId];
|
|
|
|
if (event && remainingEvents)
|
|
{
|
|
eventIdToBubbleMap[event.eventId] = bubbleData;
|
|
|
|
if (event.isLocalEvent)
|
|
{
|
|
// Listen to the identifier change for the local events.
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(localEventDidChangeIdentifier:) name:kMXEventDidChangeIdentifierNotification object:event];
|
|
}
|
|
}
|
|
}
|
|
|
|
// If there is no more events in the bubble, remove it
|
|
if (0 == remainingEvents)
|
|
{
|
|
[self removeCellData:bubbleData];
|
|
}
|
|
|
|
// Update the delegate
|
|
if (self.delegate)
|
|
{
|
|
[self.delegate dataSource:self didCellChange:nil];
|
|
}
|
|
}
|
|
|
|
- (NSArray<NSIndexPath *> *)removeCellData:(id<MXKRoomBubbleCellDataStoring>)cellData
|
|
{
|
|
NSMutableArray *deletedRows = [NSMutableArray array];
|
|
|
|
MXLogVerbose(@"[MXKRoomDataSource][%p] removeCellData: %@", self, [cellData.events valueForKey:@"eventId"]);
|
|
|
|
// Remove potential occurrences in bubble map
|
|
@synchronized (eventIdToBubbleMap)
|
|
{
|
|
for (MXEvent *event in cellData.events)
|
|
{
|
|
[eventIdToBubbleMap removeObjectForKey:event.eventId];
|
|
|
|
if (event.isLocalEvent)
|
|
{
|
|
// Stop listening to the identifier change for this event.
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidChangeIdentifierNotification object:event];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check whether the adjacent bubbles can merge together
|
|
@synchronized(bubbles)
|
|
{
|
|
NSUInteger index = [bubbles indexOfObject:cellData];
|
|
if (index != NSNotFound)
|
|
{
|
|
[bubbles removeObjectAtIndex:index];
|
|
[deletedRows addObject:[NSIndexPath indexPathForRow:index inSection:0]];
|
|
|
|
if (bubbles.count)
|
|
{
|
|
// Update flag in remaining data
|
|
if (index == 0)
|
|
{
|
|
// We removed here the first bubble.
|
|
// We have to update the 'isPaginationFirstBubble' and 'shouldHideSenderInformation' flags of the new first bubble.
|
|
id<MXKRoomBubbleCellDataStoring> firstCellData = bubbles.firstObject;
|
|
|
|
firstCellData.isPaginationFirstBubble = ((self.bubblesPagination == MXKRoomDataSourceBubblesPaginationPerDay) && firstCellData.date);
|
|
|
|
// Keep visible the sender information by default,
|
|
// except if the bubble has no display (composed only by ignored events).
|
|
firstCellData.shouldHideSenderInformation = firstCellData.hasNoDisplay;
|
|
}
|
|
else if (index < bubbles.count)
|
|
{
|
|
// We removed here a bubble which is not the before last.
|
|
id<MXKRoomBubbleCellDataStoring> cellData1 = bubbles[index-1];
|
|
id<MXKRoomBubbleCellDataStoring> cellData2 = bubbles[index];
|
|
|
|
// Check first whether the neighbor bubbles can merge
|
|
Class class = [self cellDataClassForCellIdentifier:kMXKRoomBubbleCellDataIdentifier];
|
|
if ([class instancesRespondToSelector:@selector(mergeWithBubbleCellData:)])
|
|
{
|
|
if ([cellData1 mergeWithBubbleCellData:cellData2])
|
|
{
|
|
[bubbles removeObjectAtIndex:index];
|
|
[deletedRows addObject:[NSIndexPath indexPathForRow:(index + 1) inSection:0]];
|
|
|
|
cellData2 = nil;
|
|
}
|
|
}
|
|
|
|
if (cellData2)
|
|
{
|
|
// Update its 'isPaginationFirstBubble' and 'shouldHideSenderInformation' flags
|
|
|
|
// Pagination handling
|
|
if (self.bubblesPagination == MXKRoomDataSourceBubblesPaginationPerDay && !cellData2.isPaginationFirstBubble)
|
|
{
|
|
// Check whether a new pagination starts on the second cellData
|
|
NSString *cellData1DateString = [self.eventFormatter dateStringFromDate:cellData1.date withTime:NO];
|
|
NSString *cellData2DateString = [self.eventFormatter dateStringFromDate:cellData2.date withTime:NO];
|
|
|
|
if (!cellData1DateString)
|
|
{
|
|
cellData2.isPaginationFirstBubble = (cellData2DateString && cellData.isPaginationFirstBubble);
|
|
}
|
|
else
|
|
{
|
|
cellData2.isPaginationFirstBubble = (cellData2DateString && ![cellData2DateString isEqualToString:cellData1DateString]);
|
|
}
|
|
}
|
|
|
|
// Check whether the sender information is relevant for this bubble.
|
|
// Check first if the bubble is not composed only by ignored events.
|
|
cellData2.shouldHideSenderInformation = cellData2.hasNoDisplay;
|
|
if (!cellData2.shouldHideSenderInformation && cellData2.isPaginationFirstBubble == NO)
|
|
{
|
|
// Check whether the neighbor bubbles have been sent by the same user.
|
|
cellData2.shouldHideSenderInformation = [cellData2 hasSameSenderAsBubbleCellData:cellData1];
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return deletedRows;
|
|
}
|
|
|
|
- (void)didMXRoomInitialSynced:(NSNotification *)notif
|
|
{
|
|
// Refresh the room data source when the room has been initialSync'ed
|
|
MXRoom *room = notif.object;
|
|
if (self.mxSession == room.mxSession &&
|
|
([self.roomId isEqualToString:room.roomId] || [self.secondaryRoomId isEqualToString:room.roomId]))
|
|
{
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] didMXRoomInitialSynced for room: %@", self, room.roomId);
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXRoomInitialSyncNotification object:room];
|
|
|
|
[self reload];
|
|
}
|
|
}
|
|
|
|
- (void)eventDidChangeSentState:(NSNotification *)notif
|
|
{
|
|
MXEvent *event = notif.object;
|
|
if ([event.roomId isEqualToString:_roomId])
|
|
{
|
|
MXLogVerbose(@"[MXKRoomDataSource][%p] eventDidChangeSentState: %@, to: %tu", self, event.eventId, event.sentState);
|
|
|
|
// Retrieve the cell data hosting the local echo
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData = [self cellDataOfEventWithEventId:event.eventId];
|
|
if (!bubbleData)
|
|
{
|
|
// Initial state for local echos
|
|
BOOL isInitial = event.isLocalEvent &&
|
|
(event.sentState == MXEventSentStateSending || event.sentState == MXEventSentStateEncrypting);
|
|
if (!isInitial)
|
|
{
|
|
MXLogWarning(@"[MXKRoomDataSource][%p] eventDidChangeSentState: Cannot find bubble data for event: %@", self, event.eventId);
|
|
}
|
|
return;
|
|
}
|
|
|
|
@synchronized (bubbleData)
|
|
{
|
|
[bubbleData updateEvent:event.eventId withEvent:event];
|
|
}
|
|
|
|
// Inform the delegate
|
|
if (self.delegate && (self.secondaryRoom ? bubbles.count > 0 : YES))
|
|
{
|
|
[self.delegate dataSource:self didCellChange:nil];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)localEventDidChangeIdentifier:(NSNotification *)notif
|
|
{
|
|
MXEvent *event = notif.object;
|
|
NSString *previousId = notif.userInfo[kMXEventIdentifierKey];
|
|
|
|
MXLogVerbose(@"[MXKRoomDataSource][%p] localEventDidChangeIdentifier from: %@ to: %@", self, previousId, event.eventId);
|
|
|
|
if (event && previousId)
|
|
{
|
|
// Update bubbles mapping
|
|
@synchronized (eventIdToBubbleMap)
|
|
{
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData = eventIdToBubbleMap[previousId];
|
|
if (bubbleData && event.eventId)
|
|
{
|
|
eventIdToBubbleMap[event.eventId] = bubbleData;
|
|
[eventIdToBubbleMap removeObjectForKey:previousId];
|
|
|
|
// The bubble data must use the final event id too
|
|
[bubbleData updateEvent:previousId withEvent:event];
|
|
}
|
|
}
|
|
|
|
if (!event.isLocalEvent)
|
|
{
|
|
// Stop listening to the identifier change when the event becomes an actual event.
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:kMXEventDidChangeIdentifierNotification object:event];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)eventDidDecrypt:(NSNotification *)notif
|
|
{
|
|
MXEvent *event = notif.object;
|
|
if ([event.roomId isEqualToString:_roomId] ||
|
|
([event.roomId isEqualToString:_secondaryRoomId] && [_secondaryRoomEventTypes containsObject:event.type]))
|
|
{
|
|
// Retrieve the cell data hosting the event
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData = [self cellDataOfEventWithEventId:event.eventId];
|
|
if (!bubbleData)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// We need to update the data of the cell that displays the event.
|
|
// The trickiest update is when the cell contains several events and the event
|
|
// to update turns out to be an attachment.
|
|
// In this case, we need to split the cell into several cells so that the attachment
|
|
// has its own cell.
|
|
if (bubbleData.events.count == 1 || ![_eventFormatter isSupportedAttachment:event])
|
|
{
|
|
// If the event is still a text, a simple update is enough
|
|
// If the event is an attachment, it has already its own cell. Let the bubble
|
|
// data handle the type change.
|
|
@synchronized (bubbleData)
|
|
{
|
|
[bubbleData updateEvent:event.eventId withEvent:event];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
@synchronized (bubbleData)
|
|
{
|
|
BOOL eventIsFirstInBubble = NO;
|
|
NSInteger bubbleDataIndex = [bubbles indexOfObject:bubbleData];
|
|
|
|
if (NSNotFound == bubbleDataIndex)
|
|
{
|
|
// If bubbleData is not in bubbles there is nothing to update for this event, its not displayed.
|
|
return;
|
|
}
|
|
|
|
// We need to create a dedicated cell for the event attachment.
|
|
// From the current bubble, remove the updated event and all events after.
|
|
NSMutableArray<MXEvent*> *removedEvents;
|
|
NSUInteger remainingEvents = [bubbleData removeEventsFromEvent:event.eventId removedEvents:&removedEvents];
|
|
|
|
// If there is no more events in this bubble, remove it
|
|
if (0 == remainingEvents)
|
|
{
|
|
eventIsFirstInBubble = YES;
|
|
@synchronized (eventsToProcessSnapshot)
|
|
{
|
|
[bubbles removeObjectAtIndex:bubbleDataIndex];
|
|
bubbleDataIndex--;
|
|
}
|
|
}
|
|
|
|
// Create a dedicated bubble for the attachment
|
|
if (removedEvents.count)
|
|
{
|
|
Class class = [self cellDataClassForCellIdentifier:kMXKRoomBubbleCellDataIdentifier];
|
|
|
|
id<MXKRoomBubbleCellDataStoring> newBubbleData = [[class alloc] initWithEvent:removedEvents[0] andRoomState:self.roomState andRoomDataSource:self];
|
|
|
|
if (eventIsFirstInBubble)
|
|
{
|
|
// Apply same config as before
|
|
newBubbleData.isPaginationFirstBubble = bubbleData.isPaginationFirstBubble;
|
|
newBubbleData.shouldHideSenderInformation = bubbleData.shouldHideSenderInformation;
|
|
}
|
|
else
|
|
{
|
|
// This new bubble is not the first. Show nothing
|
|
newBubbleData.isPaginationFirstBubble = NO;
|
|
newBubbleData.shouldHideSenderInformation = YES;
|
|
}
|
|
|
|
// Update bubbles mapping
|
|
@synchronized (eventIdToBubbleMap)
|
|
{
|
|
eventIdToBubbleMap[event.eventId] = newBubbleData;
|
|
}
|
|
|
|
@synchronized (eventsToProcessSnapshot)
|
|
{
|
|
[bubbles insertObject:newBubbleData atIndex:bubbleDataIndex + 1];
|
|
}
|
|
}
|
|
|
|
// And put other cutted events in another bubble
|
|
if (removedEvents.count > 1)
|
|
{
|
|
Class class = [self cellDataClassForCellIdentifier:kMXKRoomBubbleCellDataIdentifier];
|
|
|
|
id<MXKRoomBubbleCellDataStoring> newBubbleData;
|
|
for (NSUInteger i = 1; i < removedEvents.count; i++)
|
|
{
|
|
MXEvent *removedEvent = removedEvents[i];
|
|
if (i == 1)
|
|
{
|
|
newBubbleData = [[class alloc] initWithEvent:removedEvent andRoomState:self.roomState andRoomDataSource:self];
|
|
}
|
|
else
|
|
{
|
|
[newBubbleData addEvent:removedEvent andRoomState:self.roomState];
|
|
}
|
|
|
|
// Update bubbles mapping
|
|
@synchronized (eventIdToBubbleMap)
|
|
{
|
|
eventIdToBubbleMap[removedEvent.eventId] = newBubbleData;
|
|
}
|
|
}
|
|
|
|
// Do not show the
|
|
newBubbleData.isPaginationFirstBubble = NO;
|
|
newBubbleData.shouldHideSenderInformation = YES;
|
|
|
|
@synchronized (eventsToProcessSnapshot)
|
|
{
|
|
[bubbles insertObject:newBubbleData atIndex:bubbleDataIndex + 2];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the delegate
|
|
if (self.delegate)
|
|
{
|
|
[self.delegate dataSource:self didCellChange:nil];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Indicates whether an event has base requirements to allow actions (like reply, reactions, edit, etc.)
|
|
- (BOOL)canPerformActionOnEvent:(MXEvent*)event
|
|
{
|
|
BOOL isSent = event.sentState == MXEventSentStateSent;
|
|
|
|
if (!isSent) {
|
|
return NO;
|
|
}
|
|
|
|
if (event.isTimelinePollEvent) {
|
|
return YES;
|
|
}
|
|
|
|
// Specific case for voice broadcast event
|
|
if (event.eventType == MXEventTypeCustom &&
|
|
[event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) {
|
|
|
|
// Ensures that we only support reactions for a start event
|
|
VoiceBroadcastInfo* voiceBroadcastInfo = [VoiceBroadcastInfo modelFromJSON: event.content];
|
|
if ([VoiceBroadcastInfo isStartedFor: voiceBroadcastInfo.state]) {
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
BOOL isRoomMessage = (event.eventType == MXEventTypeRoomMessage);
|
|
|
|
if (!isRoomMessage) {
|
|
return NO;
|
|
}
|
|
|
|
NSString *messageType = event.content[kMXMessageTypeKey];
|
|
if (messageType == nil || [messageType isEqualToString:@"m.bad.encrypted"]) {
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)setState:(MXKDataSourceState)newState
|
|
{
|
|
if (self->state != newState)
|
|
{
|
|
self->state = newState;
|
|
|
|
if (self.delegate && [self.delegate respondsToSelector:@selector(dataSource:didStateChange:)])
|
|
{
|
|
[self.delegate dataSource:self didStateChange:self->state];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)setSecondaryRoomId:(NSString *)secondaryRoomId
|
|
{
|
|
if (_secondaryRoomId != secondaryRoomId)
|
|
{
|
|
_secondaryRoomId = secondaryRoomId;
|
|
|
|
if (self.state == MXKDataSourceStateReady)
|
|
{
|
|
[self reload];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)setSecondaryRoomEventTypes:(NSArray<MXEventTypeString> *)secondaryRoomEventTypes
|
|
{
|
|
if (_secondaryRoomEventTypes != secondaryRoomEventTypes)
|
|
{
|
|
_secondaryRoomEventTypes = secondaryRoomEventTypes;
|
|
|
|
if (self.state == MXKDataSourceStateReady)
|
|
{
|
|
[self reload];
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - Asynchronous events processing
|
|
+ (dispatch_queue_t)processingQueue
|
|
{
|
|
static dispatch_queue_t processingQueue;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
processingQueue = dispatch_queue_create("MXKRoomDataSource", DISPATCH_QUEUE_SERIAL);
|
|
});
|
|
|
|
return processingQueue;
|
|
}
|
|
|
|
- (void)queueEventForProcessing:(MXEvent*)event withRoomState:(MXRoomState*)roomState direction:(MXTimelineDirection)direction
|
|
{
|
|
if (event.isLocalEvent)
|
|
{
|
|
MXLogVerbose(@"[MXKRoomDataSource][%p] queueEventForProcessing: %@", self, event.eventId);
|
|
}
|
|
|
|
if (![self shouldQueueEventForProcessing:event roomState:roomState direction:direction])
|
|
{
|
|
return;
|
|
}
|
|
|
|
MXKQueuedEvent *queuedEvent = [[MXKQueuedEvent alloc] initWithEvent:event andRoomState:roomState direction:direction];
|
|
|
|
// Count queued events when the server sync is in progress
|
|
if (self.mxSession.state == MXSessionStateSyncInProgress)
|
|
{
|
|
queuedEvent.serverSyncEvent = YES;
|
|
_serverSyncEventCount++;
|
|
|
|
if (_serverSyncEventCount == 1)
|
|
{
|
|
// Notify that sync process starts
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKRoomDataSourceSyncStatusChanged object:self userInfo:nil];
|
|
}
|
|
}
|
|
|
|
@synchronized(eventsToProcess)
|
|
{
|
|
[eventsToProcess addObject:queuedEvent];
|
|
|
|
if (self.secondaryRoom)
|
|
{
|
|
// use a stable sorting here, which means it won't change the order of events unless it has to.
|
|
[eventsToProcess sortWithOptions:NSSortStable
|
|
usingComparator:^NSComparisonResult(MXKQueuedEvent * _Nonnull event1, MXKQueuedEvent * _Nonnull event2) {
|
|
return [event2.eventDate compare:event1.eventDate];
|
|
}];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (BOOL)canPaginate:(MXTimelineDirection)direction
|
|
{
|
|
if (_secondaryTimeline)
|
|
{
|
|
if (![_timeline canPaginate:direction] && ![_secondaryTimeline canPaginate:direction])
|
|
{
|
|
return NO;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (![_timeline canPaginate:direction])
|
|
{
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
if (direction == MXTimelineDirectionBackwards && self.shouldStopBackPagination)
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
// Check for undecryptable messages that were sent while the user was not in the room.
|
|
- (void)checkForPreJoinUTDWithEvent:(MXEvent*)event roomState:(MXRoomState*)roomState
|
|
{
|
|
// Only check for encrypted rooms
|
|
if (!self.room.summary.isEncrypted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Back pagination is stopped do not check for other pre join events
|
|
if (self.shouldStopBackPagination)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// if we reach a UTD and flag is set, hide previous encrypted messages and stop back-paginating
|
|
if (event.eventType == MXEventTypeRoomEncrypted
|
|
&& [event.decryptionError.domain isEqualToString:MXDecryptingErrorDomain]
|
|
&& self.shouldPreventBackPaginationOnPreviousUTDEvent)
|
|
{
|
|
self.shouldStopBackPagination = YES;
|
|
return;
|
|
}
|
|
|
|
self.shouldStopBackPagination = NO;
|
|
|
|
if (event.eventType != MXEventTypeRoomMember)
|
|
{
|
|
return;
|
|
}
|
|
|
|
NSString *userId = event.stateKey;
|
|
|
|
// Only check "m.room.member" event for current user
|
|
if (![userId isEqualToString:self.mxSession.myUserId])
|
|
{
|
|
return;
|
|
}
|
|
|
|
BOOL shouldPreventBackPaginationOnPreviousUTDEvent = NO;
|
|
|
|
MXRoomMember *member = [roomState.members memberWithUserId:userId];
|
|
|
|
if (member)
|
|
{
|
|
switch (member.membership) {
|
|
case MXMembershipJoin:
|
|
{
|
|
// if we reach a join event for the user:
|
|
// - if prev-content is invite, continue back-paginating
|
|
// - if prev-content is join (was just an avatar or displayname change), continue back-paginating
|
|
// - otherwise, set a flag and continue back-paginating
|
|
|
|
NSString *previousMemberhsip = event.prevContent[@"membership"];
|
|
|
|
BOOL isPrevContentAnInvite = [previousMemberhsip isEqualToString:@"invite"];
|
|
BOOL isPrevContentAJoin = [previousMemberhsip isEqualToString:@"join"];
|
|
|
|
if (!(isPrevContentAnInvite || isPrevContentAJoin))
|
|
{
|
|
shouldPreventBackPaginationOnPreviousUTDEvent = YES;
|
|
}
|
|
}
|
|
break;
|
|
case MXMembershipInvite:
|
|
// if we reach an invite event for the user, set flag and continue back-paginating
|
|
shouldPreventBackPaginationOnPreviousUTDEvent = YES;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
self.shouldPreventBackPaginationOnPreviousUTDEvent = shouldPreventBackPaginationOnPreviousUTDEvent;
|
|
}
|
|
|
|
- (BOOL)checkBing:(MXEvent*)event
|
|
{
|
|
BOOL isHighlighted = NO;
|
|
|
|
// read receipts have no rule
|
|
if (![event.type isEqualToString:kMXEventTypeStringReceipt]) {
|
|
// Check if we should bing this event
|
|
MXPushRule *rule = [self.mxSession.notificationCenter ruleMatchingEvent:event roomState:self.roomState];
|
|
if (rule)
|
|
{
|
|
// Check whether is there an highlight tweak on it
|
|
for (MXPushRuleAction *ruleAction in rule.actions)
|
|
{
|
|
if (ruleAction.actionType == MXPushRuleActionTypeSetTweak)
|
|
{
|
|
if ([ruleAction.parameters[@"set_tweak"] isEqualToString:@"highlight"])
|
|
{
|
|
// Check the highlight tweak "value"
|
|
// If not present, highlight. Else check its value before highlighting
|
|
if (nil == ruleAction.parameters[@"value"] || YES == [ruleAction.parameters[@"value"] boolValue])
|
|
{
|
|
isHighlighted = YES;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
event.mxkIsHighlighted = isHighlighted;
|
|
return isHighlighted;
|
|
}
|
|
|
|
- (void)processQueuedEvents:(void (^)(NSUInteger addedHistoryCellNb, NSUInteger addedLiveCellNb))onComplete
|
|
{
|
|
MXWeakify(self);
|
|
|
|
// Do the processing on the processing queue
|
|
dispatch_async(MXKRoomDataSource.processingQueue, ^{
|
|
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
// Note: As this block is always called from the same processing queue,
|
|
// only one batch process is done at a time. Thus, an event cannot be
|
|
// processed twice
|
|
|
|
// Snapshot queued events to avoid too long lock.
|
|
@synchronized(self->eventsToProcess)
|
|
{
|
|
if (self->eventsToProcess.count)
|
|
{
|
|
self->eventsToProcessSnapshot = self->eventsToProcess;
|
|
if (self.secondaryRoom)
|
|
{
|
|
@synchronized(self->bubbles)
|
|
{
|
|
[self->bubblesSnapshot removeAllObjects];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self->eventsToProcess = [NSMutableArray array];
|
|
}
|
|
}
|
|
}
|
|
|
|
NSUInteger serverSyncEventCount = 0;
|
|
NSUInteger addedHistoryCellCount = 0;
|
|
NSUInteger addedLiveCellCount = 0;
|
|
|
|
dispatch_group_t dispatchGroup = dispatch_group_create();
|
|
|
|
// Lock on `eventsToProcessSnapshot` to suspend reload or destroy during the process.
|
|
@synchronized(self->eventsToProcessSnapshot)
|
|
{
|
|
// Is there events to process?
|
|
// The list can be empty because several calls of processQueuedEvents may be processed
|
|
// in one pass in the processingQueue
|
|
if (self->eventsToProcessSnapshot.count)
|
|
{
|
|
// Make a quick copy of changing data to avoid to lock it too long time
|
|
@synchronized(self->bubbles)
|
|
{
|
|
self->bubblesSnapshot = [self->bubbles mutableCopy];
|
|
}
|
|
|
|
NSMutableSet<id<MXKRoomBubbleCellDataStoring>> *collapsingCellDataSeriess = [NSMutableSet set];
|
|
|
|
for (MXKQueuedEvent *queuedEvent in self->eventsToProcessSnapshot)
|
|
{
|
|
@synchronized (self->eventIdToBubbleMap)
|
|
{
|
|
// Check whether the event processed before
|
|
if (self->eventIdToBubbleMap[queuedEvent.event.eventId])
|
|
{
|
|
MXLogVerbose(@"[MXKRoomDataSource][%p] processQueuedEvents: Skip event: %@, state: %tu", self, queuedEvent.event.eventId, queuedEvent.event.sentState);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
@autoreleasepool
|
|
{
|
|
// Count events received while the server sync was in progress
|
|
if (queuedEvent.serverSyncEvent)
|
|
{
|
|
serverSyncEventCount ++;
|
|
}
|
|
|
|
// Check whether the event must be highlighted
|
|
[self checkBing:queuedEvent.event];
|
|
|
|
// Retrieve the MXKCellData class to manage the data
|
|
Class class = [self cellDataClassForCellIdentifier:kMXKRoomBubbleCellDataIdentifier];
|
|
NSAssert([class conformsToProtocol:@protocol(MXKRoomBubbleCellDataStoring)], @"MXKRoomDataSource only manages MXKCellData that conforms to MXKRoomBubbleCellDataStoring protocol");
|
|
|
|
BOOL eventManaged = NO;
|
|
BOOL updatedBubbleDataHadNoDisplay = NO;
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData;
|
|
if ([class instancesRespondToSelector:@selector(addEvent:andRoomState:)] && 0 < self->bubblesSnapshot.count)
|
|
{
|
|
// Try to concatenate the event to the last or the oldest bubble?
|
|
if (queuedEvent.direction == MXTimelineDirectionBackwards)
|
|
{
|
|
bubbleData = self->bubblesSnapshot.firstObject;
|
|
}
|
|
else
|
|
{
|
|
bubbleData = self->bubblesSnapshot.lastObject;
|
|
}
|
|
|
|
@synchronized (bubbleData)
|
|
{
|
|
updatedBubbleDataHadNoDisplay = bubbleData.hasNoDisplay;
|
|
eventManaged = [bubbleData addEvent:queuedEvent.event andRoomState:queuedEvent.state];
|
|
}
|
|
}
|
|
|
|
if (NO == eventManaged)
|
|
{
|
|
// The event has not been concatenated to an existing cell, create a new bubble for this event
|
|
bubbleData = [[class alloc] initWithEvent:queuedEvent.event andRoomState:queuedEvent.state andRoomDataSource:self];
|
|
if (!bubbleData)
|
|
{
|
|
// The event is ignored
|
|
continue;
|
|
}
|
|
|
|
// Check cells collapsing
|
|
if (bubbleData.hasAttributedTextMessage)
|
|
{
|
|
if (bubbleData.collapsable)
|
|
{
|
|
if (queuedEvent.direction == MXTimelineDirectionBackwards)
|
|
{
|
|
// Try to collapse it with the series at the start of self.bubbles
|
|
if (self->collapsableSeriesAtStart && [self->collapsableSeriesAtStart collapseWith:bubbleData])
|
|
{
|
|
// bubbleData becomes the oldest cell data of the current series
|
|
self->collapsableSeriesAtStart.prevCollapsableCellData = bubbleData;
|
|
bubbleData.nextCollapsableCellData = self->collapsableSeriesAtStart;
|
|
|
|
// The new cell must have the collapsed state as the series
|
|
bubbleData.collapsed = self->collapsableSeriesAtStart.collapsed;
|
|
|
|
// Release data of the previous header
|
|
self->collapsableSeriesAtStart.collapseState = nil;
|
|
self->collapsableSeriesAtStart.collapsedAttributedTextMessage = nil;
|
|
[collapsingCellDataSeriess removeObject:self->collapsableSeriesAtStart];
|
|
|
|
// And keep a ref of data for the new start of the series
|
|
self->collapsableSeriesAtStart = bubbleData;
|
|
self->collapsableSeriesAtStart.collapseState = queuedEvent.state;
|
|
[collapsingCellDataSeriess addObject:self->collapsableSeriesAtStart];
|
|
}
|
|
else
|
|
{
|
|
// This is a ending point for a new collapsable series of cells
|
|
self->collapsableSeriesAtStart = bubbleData;
|
|
self->collapsableSeriesAtStart.collapseState = queuedEvent.state;
|
|
[collapsingCellDataSeriess addObject:self->collapsableSeriesAtStart];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Try to collapse it with the series at the end of self.bubbles
|
|
if (self->collapsableSeriesAtEnd && [self->collapsableSeriesAtEnd collapseWith:bubbleData])
|
|
{
|
|
// Put bubbleData at the series tail
|
|
// Find the tail
|
|
id<MXKRoomBubbleCellDataStoring> tailBubbleData = self->collapsableSeriesAtEnd;
|
|
while (tailBubbleData.nextCollapsableCellData)
|
|
{
|
|
tailBubbleData = tailBubbleData.nextCollapsableCellData;
|
|
}
|
|
|
|
tailBubbleData.nextCollapsableCellData = bubbleData;
|
|
bubbleData.prevCollapsableCellData = tailBubbleData;
|
|
|
|
// The new cell must have the collapsed state as the series
|
|
bubbleData.collapsed = tailBubbleData.collapsed;
|
|
|
|
// If the start of the collapsible series stems from an event in a different processing
|
|
// batch, we need to track it here so that we can update the summary string later
|
|
if (![collapsingCellDataSeriess containsObject:self->collapsableSeriesAtEnd]) {
|
|
[collapsingCellDataSeriess addObject:self->collapsableSeriesAtEnd];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// This is a starting point for a new collapsable series of cells
|
|
self->collapsableSeriesAtEnd = bubbleData;
|
|
self->collapsableSeriesAtEnd.collapseState = queuedEvent.state;
|
|
[collapsingCellDataSeriess addObject:self->collapsableSeriesAtEnd];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The new bubble is not collapsable.
|
|
// We can close one border of the current series being built (if any)
|
|
if (queuedEvent.direction == MXTimelineDirectionBackwards && self->collapsableSeriesAtStart)
|
|
{
|
|
// This is the begin border of the series
|
|
self->collapsableSeriesAtStart = nil;
|
|
}
|
|
else if (queuedEvent.direction == MXTimelineDirectionForwards && self->collapsableSeriesAtEnd)
|
|
{
|
|
// This is the end border of the series
|
|
self->collapsableSeriesAtEnd = nil;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (queuedEvent.direction == MXTimelineDirectionBackwards)
|
|
{
|
|
// The new bubble data will be inserted at first position.
|
|
// We have to update the 'isPaginationFirstBubble' and 'shouldHideSenderInformation' flags of the current first bubble.
|
|
|
|
// Pagination handling
|
|
if ((self.bubblesPagination == MXKRoomDataSourceBubblesPaginationPerDay) && bubbleData.date)
|
|
{
|
|
// A new pagination starts with this new bubble data
|
|
bubbleData.isPaginationFirstBubble = YES;
|
|
|
|
// Check whether the current first displayed pagination title is still relevant.
|
|
if (self->bubblesSnapshot.count)
|
|
{
|
|
NSInteger index = 0;
|
|
id<MXKRoomBubbleCellDataStoring> previousFirstBubbleDataWithDate;
|
|
NSString *firstBubbleDateString;
|
|
while (index < self->bubblesSnapshot.count)
|
|
{
|
|
previousFirstBubbleDataWithDate = self->bubblesSnapshot[index++];
|
|
firstBubbleDateString = [self.eventFormatter dateStringFromDate:previousFirstBubbleDataWithDate.date withTime:NO];
|
|
|
|
if (firstBubbleDateString)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (firstBubbleDateString)
|
|
{
|
|
NSString *bubbleDateString = [self.eventFormatter dateStringFromDate:bubbleData.date withTime:NO];
|
|
previousFirstBubbleDataWithDate.isPaginationFirstBubble = (bubbleDateString && ![firstBubbleDateString isEqualToString:bubbleDateString]);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bubbleData.isPaginationFirstBubble = NO;
|
|
}
|
|
|
|
// Sender information are required for this new first bubble data,
|
|
// except if the bubble has no display (composed only by ignored events).
|
|
bubbleData.shouldHideSenderInformation = bubbleData.hasNoDisplay;
|
|
|
|
// Check whether this information is relevant for the current first bubble.
|
|
if (!bubbleData.shouldHideSenderInformation && self->bubblesSnapshot.count)
|
|
{
|
|
id<MXKRoomBubbleCellDataStoring> previousFirstBubbleData = self->bubblesSnapshot.firstObject;
|
|
|
|
if (previousFirstBubbleData.isPaginationFirstBubble == NO)
|
|
{
|
|
// Check whether the current first bubble has been sent by the same user.
|
|
previousFirstBubbleData.shouldHideSenderInformation |= [previousFirstBubbleData hasSameSenderAsBubbleCellData:bubbleData];
|
|
}
|
|
}
|
|
|
|
// Insert the new bubble data in first position
|
|
[self->bubblesSnapshot insertObject:bubbleData atIndex:0];
|
|
|
|
addedHistoryCellCount++;
|
|
}
|
|
else
|
|
{
|
|
// The new bubble data will be added at the last position
|
|
// We have to update its 'isPaginationFirstBubble' and 'shouldHideSenderInformation' flags according to the previous last bubble.
|
|
|
|
// Pagination handling
|
|
if (self.bubblesPagination == MXKRoomDataSourceBubblesPaginationPerDay)
|
|
{
|
|
// Check whether a new pagination starts at this bubble
|
|
NSString *bubbleDateString = [self.eventFormatter dateStringFromDate:bubbleData.date withTime:NO];
|
|
|
|
// Look for the current last bubble with date
|
|
NSInteger index = self->bubblesSnapshot.count;
|
|
NSString *lastBubbleDateString;
|
|
while (index--)
|
|
{
|
|
id<MXKRoomBubbleCellDataStoring> previousLastBubbleData = self->bubblesSnapshot[index];
|
|
lastBubbleDateString = [self.eventFormatter dateStringFromDate:previousLastBubbleData.date withTime:NO];
|
|
|
|
if (lastBubbleDateString)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (lastBubbleDateString)
|
|
{
|
|
bubbleData.isPaginationFirstBubble = (bubbleDateString && ![bubbleDateString isEqualToString:lastBubbleDateString]);
|
|
}
|
|
else
|
|
{
|
|
bubbleData.isPaginationFirstBubble = (bubbleDateString != nil);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bubbleData.isPaginationFirstBubble = NO;
|
|
}
|
|
|
|
// Check whether the sender information is relevant for this new bubble.
|
|
bubbleData.shouldHideSenderInformation = bubbleData.hasNoDisplay;
|
|
if (!bubbleData.shouldHideSenderInformation && self->bubblesSnapshot.count && (bubbleData.isPaginationFirstBubble == NO))
|
|
{
|
|
// Check whether the previous bubble has been sent by the same user.
|
|
id<MXKRoomBubbleCellDataStoring> previousLastBubbleData = self->bubblesSnapshot.lastObject;
|
|
bubbleData.shouldHideSenderInformation = [bubbleData hasSameSenderAsBubbleCellData:previousLastBubbleData];
|
|
}
|
|
|
|
// Insert the new bubble in last position
|
|
[self->bubblesSnapshot addObject:bubbleData];
|
|
|
|
addedLiveCellCount++;
|
|
}
|
|
}
|
|
else if (updatedBubbleDataHadNoDisplay && !bubbleData.hasNoDisplay)
|
|
{
|
|
// Here the event has been added in an existing bubble data which had no display,
|
|
// and the added event provides a display to this bubble data.
|
|
if (queuedEvent.direction == MXTimelineDirectionBackwards)
|
|
{
|
|
// The bubble is the first one.
|
|
|
|
// Pagination handling
|
|
if ((self.bubblesPagination == MXKRoomDataSourceBubblesPaginationPerDay) && bubbleData.date)
|
|
{
|
|
// A new pagination starts with this bubble data
|
|
bubbleData.isPaginationFirstBubble = YES;
|
|
|
|
// Look for the first next bubble with date to check whether its pagination title is still relevant.
|
|
if (self->bubblesSnapshot.count)
|
|
{
|
|
NSInteger index = 1;
|
|
id<MXKRoomBubbleCellDataStoring> nextBubbleDataWithDate;
|
|
NSString *firstNextBubbleDateString;
|
|
while (index < self->bubblesSnapshot.count)
|
|
{
|
|
nextBubbleDataWithDate = self->bubblesSnapshot[index++];
|
|
firstNextBubbleDateString = [self.eventFormatter dateStringFromDate:nextBubbleDataWithDate.date withTime:NO];
|
|
|
|
if (firstNextBubbleDateString)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (firstNextBubbleDateString)
|
|
{
|
|
NSString *bubbleDateString = [self.eventFormatter dateStringFromDate:bubbleData.date withTime:NO];
|
|
nextBubbleDataWithDate.isPaginationFirstBubble = (bubbleDateString && ![firstNextBubbleDateString isEqualToString:bubbleDateString]);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bubbleData.isPaginationFirstBubble = NO;
|
|
}
|
|
|
|
// Sender information are required for this new first bubble data
|
|
bubbleData.shouldHideSenderInformation = NO;
|
|
|
|
// Check whether this information is still relevant for the next bubble.
|
|
if (self->bubblesSnapshot.count > 1)
|
|
{
|
|
id<MXKRoomBubbleCellDataStoring> nextBubbleData = self->bubblesSnapshot[1];
|
|
|
|
if (nextBubbleData.isPaginationFirstBubble == NO)
|
|
{
|
|
// Check whether the current first bubble has been sent by the same user.
|
|
nextBubbleData.shouldHideSenderInformation |= [nextBubbleData hasSameSenderAsBubbleCellData:bubbleData];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The bubble data is the last one
|
|
|
|
// Pagination handling
|
|
if (self.bubblesPagination == MXKRoomDataSourceBubblesPaginationPerDay)
|
|
{
|
|
// Check whether a new pagination starts at this bubble
|
|
NSString *bubbleDateString = [self.eventFormatter dateStringFromDate:bubbleData.date withTime:NO];
|
|
|
|
// Look for the first previous bubble with date
|
|
NSInteger index = self->bubblesSnapshot.count - 1;
|
|
NSString *firstPreviousBubbleDateString;
|
|
while (index--)
|
|
{
|
|
id<MXKRoomBubbleCellDataStoring> previousBubbleData = self->bubblesSnapshot[index];
|
|
firstPreviousBubbleDateString = [self.eventFormatter dateStringFromDate:previousBubbleData.date withTime:NO];
|
|
|
|
if (firstPreviousBubbleDateString)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (firstPreviousBubbleDateString)
|
|
{
|
|
bubbleData.isPaginationFirstBubble = (bubbleDateString && ![bubbleDateString isEqualToString:firstPreviousBubbleDateString]);
|
|
}
|
|
else
|
|
{
|
|
bubbleData.isPaginationFirstBubble = (bubbleDateString != nil);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bubbleData.isPaginationFirstBubble = NO;
|
|
}
|
|
|
|
// Check whether the sender information is relevant for this new bubble.
|
|
bubbleData.shouldHideSenderInformation = NO;
|
|
if (self->bubblesSnapshot.count && (bubbleData.isPaginationFirstBubble == NO))
|
|
{
|
|
// Check whether the previous bubble has been sent by the same user.
|
|
NSInteger index = self->bubblesSnapshot.count - 1;
|
|
if (index--)
|
|
{
|
|
id<MXKRoomBubbleCellDataStoring> previousBubbleData = self->bubblesSnapshot[index];
|
|
bubbleData.shouldHideSenderInformation = [bubbleData hasSameSenderAsBubbleCellData:previousBubbleData];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[self updateCellDataReactions:bubbleData forEventId:queuedEvent.event.eventId];
|
|
|
|
// Store event-bubble link to the map
|
|
@synchronized (self->eventIdToBubbleMap)
|
|
{
|
|
self->eventIdToBubbleMap[queuedEvent.event.eventId] = bubbleData;
|
|
}
|
|
|
|
if (queuedEvent.event.isLocalEvent)
|
|
{
|
|
// Listen to the identifier change for the local events.
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(localEventDidChangeIdentifier:) name:kMXEventDidChangeIdentifierNotification object:queuedEvent.event];
|
|
}
|
|
}
|
|
}
|
|
|
|
for (MXKQueuedEvent *queuedEvent in self->eventsToProcessSnapshot)
|
|
{
|
|
@autoreleasepool
|
|
{
|
|
dispatch_group_enter(dispatchGroup);
|
|
[self addReadReceiptsForEvent:queuedEvent.event.eventId
|
|
threadId:queuedEvent.event.threadId
|
|
inCellDatas:self->bubblesSnapshot
|
|
startingAtCellData:self->eventIdToBubbleMap[queuedEvent.event.eventId] completion:^{
|
|
dispatch_group_leave(dispatchGroup);
|
|
}];
|
|
}
|
|
}
|
|
|
|
// Check if all cells of self.bubbles belongs to a single collapse series.
|
|
// In this case, collapsableSeriesAtStart and collapsableSeriesAtEnd must be equal
|
|
// in order to handle next forward or backward pagination.
|
|
if (self->collapsableSeriesAtStart && self->collapsableSeriesAtStart == self->bubbles.firstObject)
|
|
{
|
|
// Find the tail
|
|
id<MXKRoomBubbleCellDataStoring> tailBubbleData = self->collapsableSeriesAtStart;
|
|
while (tailBubbleData.nextCollapsableCellData)
|
|
{
|
|
tailBubbleData = tailBubbleData.nextCollapsableCellData;
|
|
}
|
|
|
|
if (tailBubbleData == self->bubbles.lastObject)
|
|
{
|
|
self->collapsableSeriesAtEnd = self->collapsableSeriesAtStart;
|
|
}
|
|
}
|
|
else if (self->collapsableSeriesAtEnd)
|
|
{
|
|
// Find the start
|
|
id<MXKRoomBubbleCellDataStoring> startBubbleData = self->collapsableSeriesAtEnd;
|
|
while (startBubbleData.prevCollapsableCellData)
|
|
{
|
|
startBubbleData = startBubbleData.prevCollapsableCellData;
|
|
}
|
|
|
|
if (startBubbleData == self->bubbles.firstObject)
|
|
{
|
|
self->collapsableSeriesAtStart = self->collapsableSeriesAtEnd;
|
|
}
|
|
}
|
|
|
|
// Compose (= compute collapsedAttributedTextMessage) of collapsable seriess
|
|
for (id<MXKRoomBubbleCellDataStoring> bubbleData in collapsingCellDataSeriess)
|
|
{
|
|
// Get all events of the series
|
|
NSMutableArray<MXEvent*> *events = [NSMutableArray array];
|
|
id<MXKRoomBubbleCellDataStoring> nextBubbleData = bubbleData;
|
|
do
|
|
{
|
|
[events addObjectsFromArray:nextBubbleData.events];
|
|
}
|
|
while ((nextBubbleData = nextBubbleData.nextCollapsableCellData));
|
|
|
|
// Build the summary string for the series
|
|
bubbleData.collapsedAttributedTextMessage = [self.eventFormatter attributedStringFromEvents:events
|
|
withRoomState:bubbleData.collapseState
|
|
andLatestRoomState:self.roomState
|
|
error:nil];
|
|
|
|
// Release collapseState objects, even the one of collapsableSeriesAtStart.
|
|
// We do not need to keep its state because if an collapsable event comes before collapsableSeriesAtStart,
|
|
// we will take the room state of this event.
|
|
if (bubbleData != self->collapsableSeriesAtEnd)
|
|
{
|
|
bubbleData.collapseState = nil;
|
|
}
|
|
}
|
|
}
|
|
self->eventsToProcessSnapshot = nil;
|
|
}
|
|
|
|
// Check whether some events have been processed
|
|
if (self->bubblesSnapshot)
|
|
{
|
|
// Updated data can be displayed now
|
|
// Block MXKRoomDataSource.processingQueue while the processing is finalised on the main thread
|
|
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER);
|
|
|
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
// Check whether self has not been reloaded or destroyed
|
|
if (self.state == MXKDataSourceStateReady && self->bubblesSnapshot)
|
|
{
|
|
if (self.serverSyncEventCount)
|
|
{
|
|
self->_serverSyncEventCount -= serverSyncEventCount;
|
|
if (!self.serverSyncEventCount)
|
|
{
|
|
// Notify that sync process ends
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKRoomDataSourceSyncStatusChanged object:self userInfo:nil];
|
|
}
|
|
}
|
|
if (self.secondaryRoom) {
|
|
[self->bubblesSnapshot sortWithOptions:NSSortStable
|
|
usingComparator:^NSComparisonResult(MXKRoomBubbleCellData * _Nonnull bubbleData1, MXKRoomBubbleCellData * _Nonnull bubbleData2) {
|
|
if (bubbleData1.date)
|
|
{
|
|
if (bubbleData2.date)
|
|
{
|
|
return [bubbleData1.date compare:bubbleData2.date];
|
|
}
|
|
else
|
|
{
|
|
return NSOrderedDescending;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (bubbleData2.date)
|
|
{
|
|
return NSOrderedAscending;
|
|
}
|
|
else
|
|
{
|
|
return NSOrderedSame;
|
|
}
|
|
}
|
|
}];
|
|
}
|
|
self->bubbles = self->bubblesSnapshot;
|
|
self->bubblesSnapshot = nil;
|
|
|
|
if (self.delegate)
|
|
{
|
|
[self.delegate dataSource:self didCellChange:nil];
|
|
}
|
|
else
|
|
{
|
|
// Check the memory usage of the data source. Reload it if the cache is too huge.
|
|
[self limitMemoryUsage:self.maxBackgroundCachedBubblesCount];
|
|
}
|
|
}
|
|
|
|
// Inform about the end if requested
|
|
if (onComplete)
|
|
{
|
|
onComplete(addedHistoryCellCount, addedLiveCellCount);
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
// No new event has been added, we just inform about the end if requested.
|
|
if (onComplete)
|
|
{
|
|
dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{
|
|
onComplete(0, 0);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
Add the read receipts of an event into the timeline (which is in array of cell datas)
|
|
|
|
If the event is not displayed, read receipts will be added to a previous displayed message.
|
|
|
|
@param eventId the id of the event.
|
|
@param threadId the Id of the thread related of the event.
|
|
@param cellDatas the working array of cell datas.
|
|
@param cellData the original cell data the event belongs to.
|
|
@param completion completion block
|
|
*/
|
|
- (void)addReadReceiptsForEvent:(NSString*)eventId
|
|
threadId:(NSString *)threadId
|
|
inCellDatas:(NSArray<id<MXKRoomBubbleCellDataStoring>>*)cellDatas
|
|
startingAtCellData:(id<MXKRoomBubbleCellDataStoring>)cellData
|
|
completion:(void (^)(void))completion
|
|
{
|
|
if (self.showBubbleReceipts)
|
|
{
|
|
if (self.room)
|
|
{
|
|
[self.room getEventReceipts:eventId threadId:threadId sorted:YES completion:^(NSArray<MXReceiptData *> * _Nonnull readReceipts) {
|
|
if (readReceipts.count)
|
|
{
|
|
NSInteger cellDataIndex = [cellDatas indexOfObject:cellData];
|
|
if (cellDataIndex != NSNotFound)
|
|
{
|
|
[self addReadReceipts:readReceipts forEvent:eventId inCellDatas:cellDatas atCellDataIndex:cellDataIndex];
|
|
}
|
|
}
|
|
|
|
if (!RiotSettings.shared.enableThreads)
|
|
{
|
|
// If threads are disabled, we may have several threaded RR with same userId
|
|
// but different threadId within the same timeline.
|
|
// We just need to keep the latest one.
|
|
[self clearDuplicatedReadReceiptsInCellDatas:cellDatas];
|
|
}
|
|
|
|
if (completion)
|
|
{
|
|
completion();
|
|
}
|
|
}];
|
|
}
|
|
else if (completion)
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
completion();
|
|
});
|
|
}
|
|
}
|
|
else if (completion)
|
|
{
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
completion();
|
|
});
|
|
}
|
|
}
|
|
|
|
- (void)addReadReceipts:(NSArray<MXReceiptData*> *)readReceipts forEvent:(NSString*)eventId inCellDatas:(NSArray<id<MXKRoomBubbleCellDataStoring>>*)cellDatas atCellDataIndex:(NSInteger)cellDataIndex
|
|
{
|
|
id<MXKRoomBubbleCellDataStoring> cellData = cellDatas[cellDataIndex];
|
|
|
|
if ([cellData isKindOfClass:MXKRoomBubbleCellData.class])
|
|
{
|
|
MXKRoomBubbleCellData *roomBubbleCellData = (MXKRoomBubbleCellData*)cellData;
|
|
|
|
BOOL areReadReceiptsAssigned = NO;
|
|
for (MXKRoomBubbleComponent *component in roomBubbleCellData.bubbleComponents.reverseObjectEnumerator)
|
|
{
|
|
if (component.attributedTextMessage)
|
|
{
|
|
if (roomBubbleCellData.readReceipts[component.event.eventId])
|
|
{
|
|
NSArray<MXReceiptData*> *currentReadReceipts = roomBubbleCellData.readReceipts[component.event.eventId];
|
|
NSMutableArray<MXReceiptData*> *newReadReceipts = [NSMutableArray arrayWithArray:currentReadReceipts];
|
|
for (MXReceiptData *readReceipt in readReceipts)
|
|
{
|
|
BOOL alreadyHere = NO;
|
|
for (MXReceiptData *currentReadReceipt in currentReadReceipts)
|
|
{
|
|
if ([readReceipt.userId isEqualToString:currentReadReceipt.userId])
|
|
{
|
|
alreadyHere = YES;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!alreadyHere)
|
|
{
|
|
[newReadReceipts addObject:readReceipt];
|
|
}
|
|
}
|
|
[self updateCellData:roomBubbleCellData withReadReceipts:newReadReceipts forEventId:component.event.eventId];
|
|
}
|
|
else
|
|
{
|
|
[self updateCellData:roomBubbleCellData withReadReceipts:readReceipts forEventId:component.event.eventId];
|
|
}
|
|
areReadReceiptsAssigned = YES;
|
|
break;
|
|
}
|
|
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] addReadReceipts: Read receipts for an event(%@) that is not displayed", self, eventId);
|
|
}
|
|
|
|
if (!areReadReceiptsAssigned)
|
|
{
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] addReadReceipts: Try to attach read receipts to an older message: %@", self, eventId);
|
|
|
|
// Try to assign RRs to a previous cell data
|
|
if (cellDataIndex >= 1)
|
|
{
|
|
[self addReadReceipts:readReceipts forEvent:eventId inCellDatas:cellDatas atCellDataIndex:cellDataIndex - 1];
|
|
}
|
|
else
|
|
{
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] addReadReceipts: Fail to attach read receipts for an event(%@)", self, eventId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Clear all potential duplicated RR with same user ID within a given list of cell data.
|
|
|
|
This is needed for client with threads disabled in order to clean threaded RRs.
|
|
|
|
@param cellDatas the working array of cell datas.
|
|
*/
|
|
- (void)clearDuplicatedReadReceiptsInCellDatas:(NSArray<id<MXKRoomBubbleCellDataStoring>>*)cellDatas
|
|
{
|
|
NSMutableSet<NSString *> *seenUserIds = [NSMutableSet set];
|
|
for (id<MXKRoomBubbleCellDataStoring> cellData in cellDatas.reverseObjectEnumerator)
|
|
{
|
|
if ([cellData isKindOfClass:MXKRoomBubbleCellData.class])
|
|
{
|
|
MXKRoomBubbleCellData *roomBubbleCellData = (MXKRoomBubbleCellData*)cellData;
|
|
|
|
for (MXKRoomBubbleComponent *component in roomBubbleCellData.bubbleComponents)
|
|
{
|
|
if (component.attributedTextMessage)
|
|
{
|
|
if (roomBubbleCellData.readReceipts[component.event.eventId])
|
|
{
|
|
NSArray<MXReceiptData*> *currentReadReceipts = roomBubbleCellData.readReceipts[component.event.eventId];
|
|
NSMutableArray<MXReceiptData*> *newReadReceipts = [NSMutableArray array];
|
|
for (MXReceiptData *readReceipt in currentReadReceipts)
|
|
{
|
|
if (![seenUserIds containsObject:readReceipt.userId])
|
|
{
|
|
[newReadReceipts addObject:readReceipt];
|
|
[seenUserIds addObject:readReceipt.userId];
|
|
}
|
|
}
|
|
[self updateCellData:roomBubbleCellData withReadReceipts:newReadReceipts forEventId:component.event.eventId];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark - UITableViewDataSource
|
|
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
|
{
|
|
// PATCH: Presently no bubble must be displayed until the user joins the room.
|
|
// FIXME: Handle room data source in case of room preview
|
|
if (self.room.summary.membership == MXMembershipInvite)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
NSInteger count;
|
|
@synchronized(bubbles)
|
|
{
|
|
count = bubbles.count;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
- (void)scanBubbleDataIfNeeded:(id<MXKRoomBubbleCellDataStoring>)bubbleData
|
|
{
|
|
MXScanManager *scanManager = self.mxSession.scanManager;
|
|
|
|
if (!scanManager && ![bubbleData isKindOfClass:MXKRoomBubbleCellData.class])
|
|
{
|
|
return;
|
|
}
|
|
|
|
MXKRoomBubbleCellData *roomBubbleCellData = (MXKRoomBubbleCellData*)bubbleData;
|
|
|
|
NSString *contentURL = roomBubbleCellData.attachment.contentURL;
|
|
|
|
// If the content url corresponds to an upload id, the upload is in progress or not complete.
|
|
// Create a fake event scan with in progress status when uploading media.
|
|
// Since there is no event scan in database it will be overriden by MXScanManager on media upload complete.
|
|
if (contentURL && [contentURL hasPrefix:kMXMediaUploadIdPrefix])
|
|
{
|
|
MXKRoomBubbleComponent *firstBubbleComponent = roomBubbleCellData.bubbleComponents.firstObject;
|
|
MXEvent *firstBubbleComponentEvent = firstBubbleComponent.event;
|
|
|
|
if (firstBubbleComponent && firstBubbleComponent.eventScan.antivirusScanStatus != MXAntivirusScanStatusInProgress && firstBubbleComponentEvent)
|
|
{
|
|
MXEventScan *uploadEventScan = [MXEventScan new];
|
|
uploadEventScan.eventId = firstBubbleComponentEvent.eventId;
|
|
uploadEventScan.antivirusScanStatus = MXAntivirusScanStatusInProgress;
|
|
uploadEventScan.antivirusScanDate = nil;
|
|
uploadEventScan.mediaScans = @[];
|
|
|
|
firstBubbleComponent.eventScan = uploadEventScan;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (MXKRoomBubbleComponent *bubbleComponent in roomBubbleCellData.bubbleComponents)
|
|
{
|
|
MXEvent *event = bubbleComponent.event;
|
|
|
|
if ([event isContentScannable])
|
|
{
|
|
[scanManager scanEventIfNeeded:event];
|
|
// NOTE: - [MXScanManager scanEventIfNeeded:] perform modification in background, so - [MXScanManager eventScanWithId:] do not retrieve the last state of event scan.
|
|
// It is noticeable when eventScan should be created for the first time. It would be better to return an eventScan with an in progress scan status instead of nil.
|
|
MXEventScan *eventScan = [scanManager eventScanWithId:event.eventId];
|
|
bubbleComponent.eventScan = eventScan;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
|
{
|
|
UITableViewCell<MXKCellRendering> *cell;
|
|
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData = [self cellDataAtIndex:indexPath.row];
|
|
|
|
// Launch an antivirus scan on events contained in bubble data if needed
|
|
[self scanBubbleDataIfNeeded:bubbleData];
|
|
|
|
if (bubbleData && self.delegate)
|
|
{
|
|
// Retrieve the cell identifier according to cell data.
|
|
NSString *identifier = [self.delegate cellReuseIdentifierForCellData:bubbleData];
|
|
if (identifier)
|
|
{
|
|
cell = [tableView dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath];
|
|
|
|
// Make sure we listen to user actions on the cell
|
|
cell.delegate = self;
|
|
|
|
// Update typing flag before rendering
|
|
bubbleData.isTyping = _showTypingNotifications && currentTypingUsers && ([currentTypingUsers indexOfObject:bubbleData.senderId] != NSNotFound);
|
|
// Report the current timestamp display option
|
|
bubbleData.showBubbleDateTime = self.showBubblesDateTime;
|
|
// display the read receipts
|
|
bubbleData.showBubbleReceipts = self.showBubbleReceipts;
|
|
// let the caller application manages the time label?
|
|
bubbleData.useCustomDateTimeLabel = self.useCustomDateTimeLabel;
|
|
// let the caller application manages the receipt?
|
|
bubbleData.useCustomReceipts = self.useCustomReceipts;
|
|
// let the caller application manages the unsent button?
|
|
bubbleData.useCustomUnsentButton = self.useCustomUnsentButton;
|
|
|
|
// Make the bubble display the data
|
|
[cell render:bubbleData];
|
|
}
|
|
}
|
|
|
|
// Sanity check: this method may be called during a layout refresh while room data have been modified.
|
|
if (!cell)
|
|
{
|
|
// Return an empty cell
|
|
return [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"fakeCell"];
|
|
}
|
|
|
|
return cell;
|
|
}
|
|
|
|
#pragma mark - MXScanManager notifications
|
|
|
|
- (void)registerScanManagerNotifications
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:MXScanManagerEventScanDidChangeNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(eventScansDidChange:) name:MXScanManagerEventScanDidChangeNotification object:nil];
|
|
}
|
|
|
|
- (void)unregisterScanManagerNotifications
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:MXScanManagerEventScanDidChangeNotification object:nil];
|
|
}
|
|
|
|
- (void)eventScansDidChange:(NSNotification*)notification
|
|
{
|
|
// TODO: Avoid to call the delegate to often. Set a minimum time interval to avoid table view flickering.
|
|
[self.delegate dataSource:self didCellChange:nil];
|
|
}
|
|
|
|
|
|
#pragma mark - Reactions
|
|
|
|
- (void)registerReactionsChangeListener
|
|
{
|
|
if (!self.showReactions || reactionsChangeListener)
|
|
{
|
|
return;
|
|
}
|
|
|
|
MXWeakify(self);
|
|
reactionsChangeListener = [self.mxSession.aggregations listenToReactionCountUpdateInRoom:self.roomId block:^(NSDictionary<NSString *,MXReactionCountChange *> * _Nonnull changes) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
BOOL updated = NO;
|
|
for (NSString *eventId in changes)
|
|
{
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData = [self cellDataOfEventWithEventId:eventId];
|
|
if (bubbleData)
|
|
{
|
|
// TODO: Be smarted and use changes[eventId]
|
|
[self updateCellDataReactions:bubbleData forEventId:eventId];
|
|
updated = YES;
|
|
}
|
|
}
|
|
|
|
if (updated)
|
|
{
|
|
[self.delegate dataSource:self didCellChange:nil];
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)unregisterReactionsChangeListener
|
|
{
|
|
if (reactionsChangeListener)
|
|
{
|
|
[self.mxSession.aggregations removeListener:reactionsChangeListener];
|
|
reactionsChangeListener = nil;
|
|
}
|
|
}
|
|
|
|
- (void)updateCellDataReactions:(id<MXKRoomBubbleCellDataStoring>)cellData forEventId:(NSString*)eventId
|
|
{
|
|
if (!self.showReactions || ![cellData isKindOfClass:MXKRoomBubbleCellData.class])
|
|
{
|
|
return;
|
|
}
|
|
|
|
MXKRoomBubbleCellData *roomBubbleCellData = (MXKRoomBubbleCellData*)cellData;
|
|
|
|
MXAggregatedReactions *aggregatedReactions = [self.mxSession.aggregations aggregatedReactionsOnEvent:eventId inRoom:self.roomId].aggregatedReactionsWithNonZeroCount;
|
|
|
|
if (self.showOnlySingleEmojiReactions)
|
|
{
|
|
aggregatedReactions = aggregatedReactions.aggregatedReactionsWithSingleEmoji;
|
|
}
|
|
|
|
if (aggregatedReactions)
|
|
{
|
|
if (!roomBubbleCellData.reactions)
|
|
{
|
|
roomBubbleCellData.reactions = [NSMutableDictionary dictionary];
|
|
}
|
|
|
|
roomBubbleCellData.reactions[eventId] = aggregatedReactions;
|
|
}
|
|
else
|
|
{
|
|
// unreaction
|
|
roomBubbleCellData.reactions[eventId] = nil;
|
|
}
|
|
|
|
// Indicate that the text message layout should be recomputed.
|
|
[roomBubbleCellData invalidateTextLayout];
|
|
}
|
|
|
|
- (BOOL)canReactToEventWithId:(NSString*)eventId
|
|
{
|
|
BOOL canReact = NO;
|
|
|
|
MXEvent *event = [self eventWithEventId:eventId];
|
|
|
|
if ([self canPerformActionOnEvent:event])
|
|
{
|
|
NSString *messageType = event.content[kMXMessageTypeKey];
|
|
|
|
if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest])
|
|
{
|
|
canReact = NO;
|
|
}
|
|
else
|
|
{
|
|
canReact = YES;
|
|
}
|
|
}
|
|
|
|
return canReact;
|
|
}
|
|
|
|
- (void)addReaction:(NSString *)reaction forEventId:(NSString *)eventId success:(void (^)(void))success failure:(void (^)(NSError *))failure
|
|
{
|
|
[self.mxSession.aggregations addReaction:reaction forEvent:eventId inRoom:self.roomId success:success failure:^(NSError * _Nonnull error) {
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] Fail to send reaction on eventId: %@", self, eventId);
|
|
if (failure)
|
|
{
|
|
failure(error);
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)removeReaction:(NSString *)reaction forEventId:(NSString *)eventId success:(void (^)(void))success failure:(void (^)(NSError *))failure
|
|
{
|
|
[self.mxSession.aggregations removeReaction:reaction forEvent:eventId inRoom:self.roomId success:success failure:^(NSError * _Nonnull error) {
|
|
MXLogDebug(@"[MXKRoomDataSource][%p] Fail to unreact on eventId: %@", self, eventId);
|
|
if (failure)
|
|
{
|
|
failure(error);
|
|
}
|
|
}];
|
|
}
|
|
|
|
#pragma mark - Editions
|
|
|
|
- (BOOL)canEditEventWithId:(NSString*)eventId
|
|
{
|
|
MXEvent *event = [self eventWithEventId:eventId];
|
|
BOOL isRoomMessage = event.eventType == MXEventTypeRoomMessage;
|
|
NSString *messageType = event.content[kMXMessageTypeKey];
|
|
|
|
return isRoomMessage
|
|
&& ([messageType isEqualToString:kMXMessageTypeText] || [messageType isEqualToString:kMXMessageTypeEmote])
|
|
&& [event.sender isEqualToString:self.mxSession.myUserId]
|
|
&& [event.roomId isEqualToString:self.roomId];
|
|
}
|
|
|
|
- (NSString*)editableTextMessageForEvent:(MXEvent*)event
|
|
{
|
|
NSString *editableTextMessage;
|
|
|
|
if (event.isReplyEvent)
|
|
{
|
|
MXReplyEventParser *replyEventParser = [MXReplyEventParser new];
|
|
MXReplyEventParts *replyEventParts = [replyEventParser parse:event];
|
|
|
|
editableTextMessage = replyEventParts.bodyParts.replyText;
|
|
}
|
|
else
|
|
{
|
|
editableTextMessage = event.content[kMXMessageBodyKey];
|
|
}
|
|
|
|
return editableTextMessage;
|
|
}
|
|
|
|
- (void)registerEventEditsListener
|
|
{
|
|
if (eventEditsListener)
|
|
{
|
|
return;
|
|
}
|
|
|
|
MXWeakify(self);
|
|
eventEditsListener = [self.mxSession.aggregations listenToEditsUpdateInRoom:self.roomId block:^(MXEvent * _Nonnull replaceEvent) {
|
|
MXStrongifyAndReturnIfNil(self);
|
|
|
|
[self updateEventWithReplaceEvent:replaceEvent];
|
|
}];
|
|
}
|
|
|
|
- (void)updateEventWithReplaceEvent:(MXEvent*)replaceEvent
|
|
{
|
|
NSString *editedEventId = replaceEvent.relatesTo.eventId;
|
|
|
|
dispatch_async(MXKRoomDataSource.processingQueue, ^{
|
|
|
|
// Check whether a message contains the edited event
|
|
id<MXKRoomBubbleCellDataStoring> bubbleData = [self cellDataOfEventWithEventId:editedEventId];
|
|
if (bubbleData)
|
|
{
|
|
BOOL hasChanged = [self updateCellData:bubbleData forEditionWithReplaceEvent:replaceEvent andEventId:editedEventId];
|
|
|
|
if (hasChanged)
|
|
{
|
|
// Update the delegate on main thread
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
if (self.delegate)
|
|
{
|
|
[self.delegate dataSource:self didCellChange:nil];
|
|
}
|
|
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)unregisterEventEditsListener
|
|
{
|
|
if (eventEditsListener)
|
|
{
|
|
[self.mxSession.aggregations removeListener:eventEditsListener];
|
|
eventEditsListener = nil;
|
|
}
|
|
}
|
|
|
|
- (BOOL)refreshRepliesWithUpdatedEventId:(NSString*)updatedEventId
|
|
{
|
|
BOOL hasChanged = NO;
|
|
|
|
@synchronized (bubbles) {
|
|
for (id<MXKRoomBubbleCellDataStoring> bubbleCellData in bubbles)
|
|
{
|
|
for (MXEvent *event in bubbleCellData.events)
|
|
{
|
|
if ([event.relatesTo.inReplyTo.eventId isEqual:updatedEventId])
|
|
{
|
|
[bubbleCellData updateEvent:event.eventId withEvent:event];
|
|
[bubbleCellData invalidateTextLayout];
|
|
hasChanged = YES;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return hasChanged;
|
|
}
|
|
|
|
- (BOOL)updateCellData:(id<MXKRoomBubbleCellDataStoring>)bubbleCellData forEditionWithReplaceEvent:(MXEvent*)replaceEvent andEventId:(NSString*)eventId
|
|
{
|
|
BOOL hasChanged = NO;
|
|
|
|
hasChanged = [self refreshRepliesWithUpdatedEventId:eventId];
|
|
|
|
@synchronized (bubbleCellData)
|
|
{
|
|
// Retrieve the original event to edit it
|
|
NSArray *events = bubbleCellData.events;
|
|
MXEvent *editedEvent = nil;
|
|
|
|
// If not already done, update edited event content in-place
|
|
// This is required for:
|
|
// - local echo
|
|
// - non live timeline in memory store (permalink)
|
|
for (MXEvent *event in events)
|
|
{
|
|
if ([event.eventId isEqualToString:eventId])
|
|
{
|
|
// Check whether the event was not already edited
|
|
if (![event.unsignedData.relations.replace.eventId isEqualToString:replaceEvent.eventId])
|
|
{
|
|
editedEvent = [event editedEventFromReplacementEvent:replaceEvent];
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (editedEvent)
|
|
{
|
|
if (editedEvent.sentState != replaceEvent.sentState)
|
|
{
|
|
// Relay the replace event state to the edited event so that the display
|
|
// of the edited will rerun the classic sending color flow.
|
|
// Note: this must be done on the main thread (this operation triggers
|
|
// the call of [self eventDidChangeSentState])
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
editedEvent.sentState = replaceEvent.sentState;
|
|
});
|
|
}
|
|
|
|
[bubbleCellData updateEvent:eventId withEvent:editedEvent];
|
|
[bubbleCellData invalidateTextLayout];
|
|
hasChanged = YES;
|
|
}
|
|
}
|
|
|
|
return hasChanged;
|
|
}
|
|
|
|
- (void)replaceTextMessageForEvent:(MXEvent*)event
|
|
withTextMessage:(NSString *)text
|
|
success:(void (^)(NSString *))success
|
|
failure:(void (^)(NSError *))failure
|
|
{
|
|
NSString *sanitizedText = [self sanitizedMessageText:text];
|
|
NSString *formattedText = [self htmlMessageFromSanitizedText:sanitizedText];
|
|
|
|
NSString *eventBody = event.content[kMXMessageBodyKey];
|
|
NSString *eventFormattedBody = event.content[@"formatted_body"];
|
|
|
|
if (![sanitizedText isEqualToString:eventBody] && (!eventFormattedBody || ![formattedText isEqualToString:eventFormattedBody]))
|
|
{
|
|
[self.mxSession.aggregations replaceTextMessageEvent:event withTextMessage:sanitizedText formattedText:formattedText localEchoBlock:^(MXEvent * _Nonnull replaceEventLocalEcho) {
|
|
|
|
// Apply the local echo to the timeline
|
|
[self updateEventWithReplaceEvent:replaceEventLocalEcho];
|
|
|
|
// Integrate the replace local event into the timeline like when sending a message
|
|
// This also allows to manage read receipt on this replace event
|
|
[self queueEventForProcessing:replaceEventLocalEcho withRoomState:self.roomState direction:MXTimelineDirectionForwards];
|
|
[self processQueuedEvents:nil];
|
|
|
|
} success:success failure:failure];
|
|
}
|
|
else
|
|
{
|
|
failure(nil);
|
|
}
|
|
}
|
|
|
|
#pragma mark - Virtual Rooms
|
|
|
|
- (void)virtualRoomsDidChange:(NSNotification *)notification
|
|
{
|
|
// update secondary room id
|
|
self.secondaryRoomId = [self.mxSession virtualRoomOf:self.roomId];
|
|
}
|
|
|
|
#pragma mark - Use Only Latest Profiles
|
|
|
|
/**
|
|
Refresh avatars and display names (AKA profiles) if needed.
|
|
*/
|
|
- (void)refreshProfilesIfNeeded
|
|
{
|
|
@synchronized (bubbles) {
|
|
for (id<MXKRoomBubbleCellDataStoring> bubble in bubbles)
|
|
{
|
|
[bubble refreshProfilesIfNeeded:self.roomState];
|
|
}
|
|
}
|
|
}
|
|
|
|
@end
|