/* Copyright 2015 OpenMarket Ltd Copyright 2019 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #import "RoomBubbleCellData.h" #import "EventFormatter.h" #import "AvatarGenerator.h" #import "Tools.h" #import "BubbleReactionsViewSizer.h" #import "GeneratedInterface-Swift.h" static NSAttributedString *timestampVerticalWhitespace = nil; NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotification"; @interface RoomBubbleCellData() @property(nonatomic, readonly) BOOL addVerticalWhitespaceForSelectedComponentTimestamp; @property(nonatomic, readwrite) CGFloat additionalContentHeight; @property(nonatomic) BOOL shouldUpdateAdditionalContentHeight; // Flags to "Show All" reactions for an event @property(nonatomic) NSMutableSet *eventsToShowAllReactions; @end @implementation RoomBubbleCellData - (BOOL)addVerticalWhitespaceForSelectedComponentTimestamp { return self.showTimestampForSelectedComponent && !self.displayTimestampForSelectedComponentOnLeftWhenPossible; } #pragma mark - Override MXKRoomBubbleCellData - (instancetype)init { self = [super init]; if (self) { _eventsToShowAllReactions = [NSMutableSet set]; _componentIndexOfSentMessageTick = -1; } return self; } - (instancetype)initWithEvent:(MXEvent *)event andRoomState:(MXRoomState *)roomState andRoomDataSource:(MXKRoomDataSource *)roomDataSource2 { self = [super initWithEvent:event andRoomState:roomState andRoomDataSource:roomDataSource2]; if (self) { self.displayTimestampForSelectedComponentOnLeftWhenPossible = YES; switch (event.eventType) { case MXEventTypeRoomMember: { // Membership events have their own cell type self.tag = RoomBubbleCellDataTagMembership; // Membership events can be collapsed together self.collapsable = YES; // Collapse them by default self.collapsed = YES; // find the room create event in stateEvents MXEvent *roomCreateEvent = [roomState.stateEvents filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"wireType == %@", kMXEventTypeStringRoomCreate]].firstObject; NSString *creatorUserId = [MXRoomCreateContent modelFromJSON:roomCreateEvent.content].creatorUserId; if (creatorUserId) { MXRoomMemberEventContent *content = [MXRoomMemberEventContent modelFromJSON:event.content]; if ([kMXMembershipStringJoin isEqualToString:content.membership] && [creatorUserId isEqualToString:event.sender]) { // join event of the room creator // group it with room creation events self.tag = RoomBubbleCellDataTagRoomCreateConfiguration; } } } break; case MXEventTypeRoomCreate: { MXRoomCreateContent *createContent = [MXRoomCreateContent modelFromJSON:event.content]; if (createContent.roomPredecessorInfo) { self.tag = RoomBubbleCellDataTagRoomCreateWithPredecessor; } else { self.tag = RoomBubbleCellDataTagRoomCreateConfiguration; } // Membership events can be collapsed together self.collapsable = YES; // Collapse them by default self.collapsed = YES; } break; case MXEventTypeRoomTopic: case MXEventTypeRoomName: case MXEventTypeRoomEncryption: case MXEventTypeRoomHistoryVisibility: case MXEventTypeRoomGuestAccess: case MXEventTypeRoomAvatar: case MXEventTypeRoomJoinRules: { self.tag = RoomBubbleCellDataTagRoomCreateConfiguration; // Membership events can be collapsed together self.collapsable = YES; // Collapse them by default self.collapsed = YES; } break; case MXEventTypeCallInvite: case MXEventTypeCallAnswer: case MXEventTypeCallHangup: case MXEventTypeCallReject: { self.tag = RoomBubbleCellDataTagCall; // Call events can be collapsed together self.collapsable = YES; // Collapse them by default self.collapsed = YES; // Show timestamps always on right self.displayTimestampForSelectedComponentOnLeftWhenPossible = NO; break; } case MXEventTypePollStart: { self.tag = RoomBubbleCellDataTagPoll; self.collapsable = NO; self.collapsed = NO; break; } case MXEventTypeCustom: { if ([event.type isEqualToString:kWidgetMatrixEventTypeString] || [event.type isEqualToString:kWidgetModularEventTypeString]) { Widget *widget = [[Widget alloc] initWithWidgetEvent:event inMatrixSession:roomDataSource.mxSession]; if ([widget.type isEqualToString:kWidgetTypeJitsiV1] || [widget.type isEqualToString:kWidgetTypeJitsiV2]) { self.tag = RoomBubbleCellDataTagGroupCall; // Show timestamps always on right self.displayTimestampForSelectedComponentOnLeftWhenPossible = NO; } } break; } case MXEventTypeRoomMessage: { if (event.location) { self.tag = RoomBubbleCellDataTagLocation; self.collapsable = NO; self.collapsed = NO; } } default: break; } [self keyVerificationDidUpdate]; // Increase maximum number of components self.maxComponentCount = 20; // Indicate that the text message layout should be recomputed. [self invalidateTextLayout]; // Load a url preview if necessary. [self refreshURLPreviewForEventId:event.eventId]; } return self; } - (NSUInteger)updateEvent:(NSString *)eventId withEvent:(MXEvent *)event { NSUInteger retVal = [super updateEvent:eventId withEvent:event]; // Update any URL preview data as necessary. [self refreshURLPreviewForEventId:event.eventId]; return retVal; } - (void)prepareBubbleComponentsPosition { if (shouldUpdateComponentsPosition) { // The bubble layout depends on the room read receipts which must be retrieved on the main thread to prevent us from race conditions. // Check here the current thread, this is just a sanity check because this method is called during the rendering step // which takes place on the main thread. if ([NSThread currentThread] != [NSThread mainThread]) { MXLogDebug(@"[RoomBubbleCellData] prepareBubbleComponentsPosition called on wrong thread"); dispatch_sync(dispatch_get_main_queue(), ^{ [self refreshBubbleComponentsPosition]; }); } else { [self refreshBubbleComponentsPosition]; } shouldUpdateComponentsPosition = NO; } [self updateAdditionalContentHeightIfNeeded]; } - (NSAttributedString*)attributedTextMessage { @synchronized(bubbleComponents) { if (self.hasAttributedTextMessage && !attributedTextMessage.length) { // Attributed text message depends on the room read receipts which must be retrieved on the main thread to prevent us from race conditions. // Check here the current thread, this is just a sanity check because the attributed text message // is requested during the rendering step which takes place on the main thread. if ([NSThread currentThread] != [NSThread mainThread]) { MXLogDebug(@"[RoomBubbleCellData] attributedTextMessage called on wrong thread"); dispatch_sync(dispatch_get_main_queue(), ^{ self.attributedTextMessage = [self makeAttributedString]; }); } else { self.attributedTextMessage = [self makeAttributedString]; } } } return attributedTextMessage; } - (BOOL)hasNoDisplay { if (self.tag == RoomBubbleCellDataTagKeyVerificationNoDisplay) { return YES; } if (self.tag == RoomBubbleCellDataTagRoomCreationIntro) { return NO; } if (self.tag == RoomBubbleCellDataTagPoll) { return NO; } if (self.tag == RoomBubbleCellDataTagLocation) { return NO; } return [super hasNoDisplay]; } #pragma mark - Bubble collapsing - (BOOL)collapseWith:(id)cellData { if (self.tag == RoomBubbleCellDataTagMembership && cellData.tag == RoomBubbleCellDataTagMembership) { // For now, do not merge VoIP conference events if (![MXCallManager isConferenceUser:cellData.events.firstObject.stateKey]) { // Keep a pagination between events of different days NSString *bubbleDateString = [roomDataSource.eventFormatter dateStringFromDate:self.date withTime:NO]; NSString *eventDateString = [roomDataSource.eventFormatter dateStringFromDate:((RoomBubbleCellData*)cellData).date withTime:NO]; if (bubbleDateString && eventDateString && [bubbleDateString isEqualToString:eventDateString]) { return YES; } } return NO; } else if (self.tag == RoomBubbleCellDataTagRoomCreateConfiguration && cellData.tag == RoomBubbleCellDataTagRoomCreateConfiguration) { return YES; } else if (self.tag == RoomBubbleCellDataTagCall && cellData.tag == RoomBubbleCellDataTagCall) { // Check if the same call MXEvent * event1 = self.events.firstObject; MXCallEventContent *eventContent1 = [MXCallEventContent modelFromJSON:event1.content]; MXEvent * event2 = cellData.events.firstObject; MXCallEventContent *eventContent2 = [MXCallEventContent modelFromJSON:event2.content]; return [eventContent1.callId isEqualToString:eventContent2.callId]; } if (self.tag == RoomBubbleCellDataTagRoomCreateWithPredecessor || cellData.tag == RoomBubbleCellDataTagRoomCreateWithPredecessor) { return NO; } return [super collapseWith:cellData]; } - (void)setCollapsed:(BOOL)collapsed { if (collapsed != self.collapsed) { super.collapsed = collapsed; // Refresh only cells series header if (self.collapsedAttributedTextMessage && self.nextCollapsableCellData) { [self invalidateTextLayout]; } } } #pragma mark - - (void)invalidateLayout { [self invalidateTextLayout]; [self setNeedsUpdateAdditionalContentHeight]; } - (NSAttributedString*)makeAttributedString { // CAUTION: This method must be called on the main thread. // Return the collapsed string only for cells series header if (self.collapsed && self.collapsedAttributedTextMessage && self.nextCollapsableCellData) { return super.collapsedAttributedTextMessage; } NSMutableAttributedString *currentAttributedTextMsg; NSInteger selectedComponentIndex = self.selectedComponentIndex; NSInteger lastMessageIndex = self.containsLastMessage ? self.mostRecentComponentIndex : NSNotFound; MXKRoomBubbleComponent *component; NSAttributedString *componentString; NSUInteger index = 0; for (; index < bubbleComponents.count; index++) { component = bubbleComponents[index]; componentString = component.attributedTextMessage; if (componentString) { // Check whether another component than this one is selected // Note: When a component is selected, it is highlighted by applying an alpha on other components. if (selectedComponentIndex != NSNotFound && selectedComponentIndex != index && componentString.length) { // Apply alpha to blur this component componentString = [Tools setTextColorAlpha:.2 inAttributedString:componentString]; } // Check whether the timestamp is displayed for this component, and check whether a vertical whitespace is required if (((selectedComponentIndex == index && self.addVerticalWhitespaceForSelectedComponentTimestamp) || lastMessageIndex == index) && (self.shouldHideSenderInformation || self.shouldHideSenderName)) { currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; [currentAttributedTextMsg appendAttributedString:componentString]; } else { // Init attributed string with the first text component currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:componentString]; } [self addVerticalWhitespaceToString:currentAttributedTextMsg forEvent:component.event.eventId]; // The first non empty component has been handled. break; } } for (index++; index < bubbleComponents.count; index++) { component = bubbleComponents[index]; componentString = component.attributedTextMessage; if (componentString) { [currentAttributedTextMsg appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; // Check whether another component than this one is selected if (selectedComponentIndex != NSNotFound && selectedComponentIndex != index && componentString.length) { // Apply alpha to blur this component componentString = [Tools setTextColorAlpha:.2 inAttributedString:componentString]; } // Check whether the timestamp is displayed if ((selectedComponentIndex == index && self.addVerticalWhitespaceForSelectedComponentTimestamp) || lastMessageIndex == index) { [currentAttributedTextMsg appendAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; } // Append attributed text [currentAttributedTextMsg appendAttributedString:componentString]; [self addVerticalWhitespaceToString:currentAttributedTextMsg forEvent:component.event.eventId]; } } return currentAttributedTextMsg; } - (NSInteger)firstVisibleComponentIndex { __block NSInteger firstVisibleComponentIndex = NSNotFound; BOOL isPoll = (self.events.firstObject.eventType == MXEventTypePollStart); if ((isPoll || self.attachment) && self.bubbleComponents.count) { firstVisibleComponentIndex = 0; } else { [self.bubbleComponents enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { MXKRoomBubbleComponent *component = (MXKRoomBubbleComponent*)obj; if (component.attributedTextMessage) { firstVisibleComponentIndex = idx; *stop = YES; } }]; } return firstVisibleComponentIndex; } - (void)refreshBubbleComponentsPosition { // CAUTION: This method must be called on the main thread. @synchronized(bubbleComponents) { NSInteger bubbleComponentsCount = bubbleComponents.count; // Check whether there is at least one component. if (bubbleComponentsCount) { // Set position of the first component CGFloat positionY = (self.attachment == nil || self.attachment.type == MXKAttachmentTypeFile || self.attachment.type == MXKAttachmentTypeAudio) ? MXKROOMBUBBLECELLDATA_TEXTVIEW_DEFAULT_VERTICAL_INSET : 0; MXKRoomBubbleComponent *component; NSUInteger index = 0; // Use same position for first components without render (redacted) for (; index < bubbleComponentsCount; index++) { // Compute the vertical position for next component component = bubbleComponents[index]; component.position = CGPointMake(0, positionY); if (component.attributedTextMessage) { break; } } // Check whether the position of other components need to be refreshed if (!self.attachment && index < bubbleComponentsCount) { NSMutableAttributedString *attributedString = [NSMutableAttributedString new]; NSInteger selectedComponentIndex = self.selectedComponentIndex; NSInteger lastMessageIndex = self.containsLastMessage ? self.mostRecentComponentIndex : NSNotFound; NSInteger visibleMessageIndex = 0; for (; index < bubbleComponentsCount; index++) { // Compute the vertical position for next component component = bubbleComponents[index]; if (component.attributedTextMessage) { // Prepare its attributed string by considering potential vertical margin required to display timestamp. NSAttributedString *componentString = component.attributedTextMessage; // Check whether the timestamp is displayed for this component, and check whether a vertical whitespace is required if (((selectedComponentIndex == index && self.addVerticalWhitespaceForSelectedComponentTimestamp) || lastMessageIndex == index) && !(visibleMessageIndex == 0 && !(self.shouldHideSenderInformation || self.shouldHideSenderName))) { [attributedString appendAttributedString:[RoomBubbleCellData timestampVerticalWhitespace]]; } // Append this attributed string. [attributedString appendAttributedString:componentString]; // Compute the height of the resulting string. CGFloat cumulatedHeight = [self rawTextHeight:attributedString]; // Deduce the position of the beginning of this component. positionY = MXKROOMBUBBLECELLDATA_TEXTVIEW_DEFAULT_VERTICAL_INSET + (cumulatedHeight - [self rawTextHeight:componentString]); component.position = CGPointMake(0, positionY); // Vertical whitespace is added in case of read receipts or reactions [self addVerticalWhitespaceToString:attributedString forEvent:component.event.eventId]; [attributedString appendAttributedString:[MXKRoomBubbleCellDataWithAppendingMode messageSeparator]]; visibleMessageIndex++; } else { component.position = CGPointMake(0, positionY); } } } } } } - (void)addVerticalWhitespaceToString:(NSMutableAttributedString *)attributedString forEvent:(NSString *)eventId { CGFloat additionalVerticalHeight = 0; // Add vertical whitespace in case of a url preview. additionalVerticalHeight+= [self urlPreviewHeightForEventId:eventId]; // Add vertical whitespace in case of reactions. additionalVerticalHeight+= [self reactionHeightForEventId:eventId]; // Add vertical whitespace in case of read receipts. additionalVerticalHeight+= [self readReceiptHeightForEventId:eventId]; if (additionalVerticalHeight) { [attributedString appendAttributedString:[RoomBubbleCellData verticalWhitespaceForHeight: additionalVerticalHeight]]; } } - (CGFloat)computeAdditionalHeight { CGFloat height = 0; for (MXKRoomBubbleComponent *bubbleComponent in self.bubbleComponents) { NSString *eventId = bubbleComponent.event.eventId; height+= [self urlPreviewHeightForEventId:eventId]; height+= [self reactionHeightForEventId:eventId]; height+= [self readReceiptHeightForEventId:eventId]; } return height; } - (void)updateAdditionalContentHeightIfNeeded; { if (self.shouldUpdateAdditionalContentHeight) { void(^updateAdditionalHeight)(void) = ^() { self.additionalContentHeight = [self computeAdditionalHeight]; }; // The additional height depends on the room read receipts and reactions view which must be calculated on the main thread. // Check here the current thread, this is just a sanity check because this method is called during the rendering step // which takes place on the main thread. if ([NSThread currentThread] != [NSThread mainThread]) { MXLogDebug(@"[RoomBubbleCellData] prepareBubbleComponentsPosition called on wrong thread"); dispatch_sync(dispatch_get_main_queue(), ^{ updateAdditionalHeight(); }); } else { updateAdditionalHeight(); } self.shouldUpdateAdditionalContentHeight = NO; } } - (void)setNeedsUpdateAdditionalContentHeight { self.shouldUpdateAdditionalContentHeight = YES; } - (CGFloat)urlPreviewHeightForEventId:(NSString*)eventId { MXKRoomBubbleComponent *component = [self bubbleComponentWithLinkForEventId:eventId]; if (!component.showURLPreview) { return 0; } return RoomBubbleCellLayout.urlPreviewViewTopMargin + [URLPreviewView contentViewHeightFor:component.urlPreviewData fitting:self.maxTextViewWidth]; } - (CGFloat)reactionHeightForEventId:(NSString*)eventId { CGFloat height = 0; NSUInteger reactionCount = self.reactions[eventId].reactions.count; MXAggregatedReactions *aggregatedReactions = self.reactions[eventId]; if (reactionCount) { CGFloat bubbleReactionsViewWidth = self.maxTextViewWidth - 4; static BubbleReactionsViewSizer *bubbleReactionsViewSizer; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ bubbleReactionsViewSizer = [BubbleReactionsViewSizer new]; }); BOOL showAllReactions = [self.eventsToShowAllReactions containsObject:eventId]; BubbleReactionsViewModel *viewModel = [[BubbleReactionsViewModel alloc] initWithAggregatedReactions:aggregatedReactions eventId:eventId showAll:showAllReactions]; height = [bubbleReactionsViewSizer heightForViewModel:viewModel fittingWidth:bubbleReactionsViewWidth] + RoomBubbleCellLayout.reactionsViewTopMargin; } return height; } - (CGFloat)readReceiptHeightForEventId:(NSString*)eventId { CGFloat height = 0; if (self.readReceipts[eventId].count) { height = RoomBubbleCellLayout.readReceiptsViewHeight + RoomBubbleCellLayout.readReceiptsViewTopMargin; } return height; } - (void)setContainsLastMessage:(BOOL)containsLastMessage { // Check whether there is something to do if (_containsLastMessage || containsLastMessage) { // Update flag _containsLastMessage = containsLastMessage; // Indicate that the text message layout should be recomputed. [self invalidateTextLayout]; } } - (void)setSelectedEventId:(NSString *)selectedEventId { // Check whether there is something to do if (_selectedEventId || selectedEventId.length) { // Update flag _selectedEventId = selectedEventId; // Indicate that the text message layout should be recomputed. [self invalidateTextLayout]; } } - (NSInteger)oldestComponentIndex { // Update the related component index NSInteger oldestComponentIndex = NSNotFound; NSArray *components = self.bubbleComponents; NSInteger index = 0; while (index < components.count) { MXKRoomBubbleComponent *component = components[index]; if (component.attributedTextMessage && component.date) { oldestComponentIndex = index; break; } index++; } return oldestComponentIndex; } - (NSInteger)mostRecentComponentIndex { // Update the related component index NSInteger mostRecentComponentIndex = NSNotFound; NSArray *components = self.bubbleComponents; NSInteger index = components.count; while (index--) { MXKRoomBubbleComponent *component = components[index]; if (component.attributedTextMessage && component.date) { mostRecentComponentIndex = index; break; } } return mostRecentComponentIndex; } - (NSInteger)selectedComponentIndex { // Update the related component index NSInteger selectedComponentIndex = NSNotFound; if (_selectedEventId) { NSArray *components = self.bubbleComponents; NSInteger index = components.count; while (index--) { MXKRoomBubbleComponent *component = components[index]; if ([component.event.eventId isEqualToString:_selectedEventId]) { selectedComponentIndex = index; break; } } } return selectedComponentIndex; } - (MXKRoomBubbleComponent *)bubbleComponentWithLinkForEventId:(NSString *)eventId { NSInteger index = [self bubbleComponentIndexForEventId:eventId]; if (index == NSNotFound) { return nil; } MXKRoomBubbleComponent *component = self.bubbleComponents[index]; if (!component.link) { return nil; } return component; } #pragma mark - + (NSAttributedString *)timestampVerticalWhitespace { @synchronized(self) { if (timestampVerticalWhitespace == nil) { timestampVerticalWhitespace = [[NSAttributedString alloc] initWithString:@"\n" attributes:@{NSForegroundColorAttributeName : [UIColor blackColor], NSFontAttributeName: [UIFont systemFontOfSize:12]}]; } } return timestampVerticalWhitespace; } + (NSAttributedString *)verticalWhitespaceForHeight:(CGFloat)height { UIFont *sizingFont = [UIFont systemFontOfSize:2]; CGFloat returnHeight = sizingFont.lineHeight; NSUInteger returns = (NSUInteger)round(height/returnHeight); NSMutableString *returnString = [NSMutableString string]; for (NSUInteger i = 0; i < returns; i++) { [returnString appendString:@"\n"]; } return [[NSAttributedString alloc] initWithString:returnString attributes:@{NSForegroundColorAttributeName : [UIColor blackColor], NSFontAttributeName: sizingFont}]; } - (BOOL)hasSameSenderAsBubbleCellData:(id)bubbleCellData { if (self.tag == RoomBubbleCellDataTagMembership || bubbleCellData.tag == RoomBubbleCellDataTagMembership) { // We do not want to merge membership event cells with other cell types return NO; } if (self.tag == RoomBubbleCellDataTagRoomCreateWithPredecessor || bubbleCellData.tag == RoomBubbleCellDataTagRoomCreateWithPredecessor) { // We do not want to merge room create event cells with other cell types return NO; } return [super hasSameSenderAsBubbleCellData:bubbleCellData]; } - (BOOL)addEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState { BOOL shouldAddEvent = YES; switch (self.tag) { case RoomBubbleCellDataTagKeyVerificationNoDisplay: case RoomBubbleCellDataTagKeyVerificationRequest: case RoomBubbleCellDataTagKeyVerificationRequestIncomingApproval: case RoomBubbleCellDataTagKeyVerificationConclusion: shouldAddEvent = NO; break; case RoomBubbleCellDataTagRoomCreateWithPredecessor: // We do not want to merge room create event cells with other cell types shouldAddEvent = NO; break; case RoomBubbleCellDataTagMembership: // One single bubble per membership event shouldAddEvent = NO; break; case RoomBubbleCellDataTagCall: shouldAddEvent = NO; break; case RoomBubbleCellDataTagGroupCall: shouldAddEvent = NO; break; case RoomBubbleCellDataTagRoomCreateConfiguration: shouldAddEvent = NO; break; case RoomBubbleCellDataTagRoomCreationIntro: shouldAddEvent = NO; break; case RoomBubbleCellDataTagPoll: shouldAddEvent = NO; break; case RoomBubbleCellDataTagLocation: shouldAddEvent = NO; break; default: break; } // If the current bubbleData supports adding events then check // if the incoming event can be added in if (shouldAddEvent) { switch (event.eventType) { case MXEventTypeRoomMessage: { if (event.location) { shouldAddEvent = NO; break; } NSString *messageType = event.content[kMXMessageTypeKey]; if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { shouldAddEvent = NO; } break; } case MXEventTypeKeyVerificationStart: case MXEventTypeKeyVerificationAccept: case MXEventTypeKeyVerificationKey: case MXEventTypeKeyVerificationMac: case MXEventTypeKeyVerificationDone: case MXEventTypeKeyVerificationCancel: shouldAddEvent = NO; break; case MXEventTypeRoomMember: shouldAddEvent = NO; break; case MXEventTypeRoomCreate: shouldAddEvent = NO; break; case MXEventTypeRoomTopic: case MXEventTypeRoomName: case MXEventTypeRoomEncryption: case MXEventTypeRoomHistoryVisibility: case MXEventTypeRoomGuestAccess: case MXEventTypeRoomAvatar: case MXEventTypeRoomJoinRules: shouldAddEvent = NO; break; case MXEventTypeCallInvite: case MXEventTypeCallAnswer: case MXEventTypeCallHangup: case MXEventTypeCallReject: shouldAddEvent = NO; break; case MXEventTypePollStart: shouldAddEvent = NO; break; case MXEventTypeCustom: { if ([event.type isEqualToString:kWidgetMatrixEventTypeString] || [event.type isEqualToString:kWidgetModularEventTypeString]) { Widget *widget = [[Widget alloc] initWithWidgetEvent:event inMatrixSession:roomDataSource.mxSession]; if ([widget.type isEqualToString:kWidgetTypeJitsiV1] || [widget.type isEqualToString:kWidgetTypeJitsiV2]) { shouldAddEvent = NO; } } break; } default: break; } } if (shouldAddEvent) { shouldAddEvent = [super addEvent:event andRoomState:roomState]; // If the event was added, load any url preview data if necessary. if (shouldAddEvent) { [self refreshURLPreviewForEventId:event.eventId]; } } return shouldAddEvent; } - (void)setKeyVerification:(MXKeyVerification *)keyVerification { _keyVerification = keyVerification; [self keyVerificationDidUpdate]; } - (void)keyVerificationDidUpdate { MXEvent *event = self.getFirstBubbleComponentWithDisplay.event; MXKeyVerification *keyVerification = _keyVerification; if (!event) { return; } switch (event.eventType) { case MXEventTypeKeyVerificationCancel: { RoomBubbleCellDataTag cellDataTag; MXTransactionCancelCode *transactionCancelCode = keyVerification.transaction.reasonCancelCode; if (transactionCancelCode && ([transactionCancelCode isEqual:[MXTransactionCancelCode mismatchedSas]] || [transactionCancelCode isEqual:[MXTransactionCancelCode mismatchedKeys]] || [transactionCancelCode isEqual:[MXTransactionCancelCode mismatchedCommitment]] ) ) { cellDataTag = RoomBubbleCellDataTagKeyVerificationConclusion; } else { cellDataTag = RoomBubbleCellDataTagKeyVerificationNoDisplay; } self.tag = cellDataTag; } break; case MXEventTypeKeyVerificationDone: { RoomBubbleCellDataTag cellDataTag; // Avoid to display incoming and outgoing done, only display the incoming one. if (self.isIncoming && keyVerification && (keyVerification.state == MXKeyVerificationStateVerified)) { cellDataTag = RoomBubbleCellDataTagKeyVerificationConclusion; } else { cellDataTag = RoomBubbleCellDataTagKeyVerificationNoDisplay; } self.tag = cellDataTag; } break; case MXEventTypeRoomMessage: { NSString *msgType = event.content[kMXMessageTypeKey]; if ([msgType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { RoomBubbleCellDataTag cellDataTag; if (self.isIncoming && !self.isKeyVerificationOperationPending && keyVerification && keyVerification.state == MXKeyVerificationRequestStatePending) { cellDataTag = RoomBubbleCellDataTagKeyVerificationRequestIncomingApproval; } else { cellDataTag = RoomBubbleCellDataTagKeyVerificationRequest; } self.tag = cellDataTag; } } break; default: break; } } #pragma mark - Show all reactions - (BOOL)showAllReactionsForEvent:(NSString*)eventId { return [self.eventsToShowAllReactions containsObject:eventId]; } - (void)setShowAllReactions:(BOOL)showAllReactions forEvent:(NSString*)eventId { if (showAllReactions) { [self.eventsToShowAllReactions addObject:eventId]; } else { [self.eventsToShowAllReactions removeObject:eventId]; } } - (NSString *)accessibilityLabel { NSString *accessibilityLabel; // Only media require manual handling for accessibility if (self.attachment) { NSString *mediaName = [self accessibilityLabelForAttachmentType:self.attachment.type]; MXJSONModelSetString(accessibilityLabel, self.events.firstObject.content[kMXMessageBodyKey]); if (accessibilityLabel) { accessibilityLabel = [NSString stringWithFormat:@"%@ %@", mediaName, accessibilityLabel]; } else { accessibilityLabel = mediaName; } } return accessibilityLabel; } - (NSString*)accessibilityLabelForAttachmentType:(MXKAttachmentType)attachmentType { NSString *accessibilityLabel; switch (attachmentType) { case MXKAttachmentTypeImage: accessibilityLabel = [VectorL10n mediaTypeAccessibilityImage]; break; case MXKAttachmentTypeAudio: accessibilityLabel = [VectorL10n mediaTypeAccessibilityAudio]; break; case MXKAttachmentTypeVoiceMessage: accessibilityLabel = [VectorL10n mediaTypeAccessibilityAudio]; break; case MXKAttachmentTypeVideo: accessibilityLabel = [VectorL10n mediaTypeAccessibilityVideo]; break; case MXKAttachmentTypeFile: accessibilityLabel = [VectorL10n mediaTypeAccessibilityFile]; break; case MXKAttachmentTypeSticker: accessibilityLabel = [VectorL10n mediaTypeAccessibilitySticker]; break; default: accessibilityLabel = @""; break; } return accessibilityLabel; } #pragma mark - URL Previews - (void)refreshURLPreviewForEventId:(NSString *)eventId { // Get the event's component, but only if it has a link. MXKRoomBubbleComponent *component = [self bubbleComponentWithLinkForEventId:eventId]; if (!component) { return; } // Don't show the preview if they're disabled globally or this one has been dismissed previously. component.showURLPreview = RiotSettings.shared.roomScreenShowsURLPreviews && [URLPreviewService.shared shouldShowPreviewFor:component.event]; if (!component.showURLPreview) { return; } // If there is existing preview data, the message has been edited. // Clear the data to show the loading state when the preview isn't cached. if (component.urlPreviewData) { component.urlPreviewData = nil; } // Set the preview data. MXWeakify(self); NSDictionary *userInfo = @{ @"eventId": eventId, @"roomId": self.roomId }; [URLPreviewService.shared previewFor:component.link and:component.event with:self.mxSession success:^(URLPreviewData * _Nonnull urlPreviewData) { MXStrongifyAndReturnIfNil(self); // Update the preview data, indicate that the message layout needs refreshing and send a notification for refresh component.urlPreviewData = urlPreviewData; [self invalidateLayout]; dispatch_async(dispatch_get_main_queue(), ^{ [NSNotificationCenter.defaultCenter postNotificationName:URLPreviewDidUpdateNotification object:nil userInfo:userInfo]; }); } failure:^(NSError * _Nullable error) { MXStrongifyAndReturnIfNil(self); MXLogDebug(@"[RoomBubbleCellData] Failed to get url preview") // Remove the loading URLPreviewView, indicate that the layout needs refreshing and send a notification for refresh component.showURLPreview = NO; [self invalidateLayout]; dispatch_async(dispatch_get_main_queue(), ^{ [NSNotificationCenter.defaultCenter postNotificationName:URLPreviewDidUpdateNotification object:nil userInfo:userInfo]; }); }]; } @end