In reply to %@
%@
In reply to // By ['In reply to' from resources] // To disable the link and to localize the "In reply to" string // This link is the first HTML node of the html string if (inReplyToTextRange.location != NSNotFound) { html = [html stringByReplacingCharactersInRange:inReplyToTextRange withString:[MatrixKitL10n noticeInReplyTo]]; } if (inReplyToLinkRange.location != NSNotFound) { html = [html stringByReplacingCharactersInRange:inReplyToLinkRange withString:@"#"]; } return html; } - (NSAttributedString*)postRenderAttributedString:(NSAttributedString*)attributedString { if (!attributedString) { return nil; } NSInteger enabledMatrixIdsBitMask= 0; // If enabled, make user id clickable if (_treatMatrixUserIdAsLink) { enabledMatrixIdsBitMask |= MXKTOOLS_USER_IDENTIFIER_BITWISE; } // If enabled, make room id clickable if (_treatMatrixRoomIdAsLink) { enabledMatrixIdsBitMask |= MXKTOOLS_ROOM_IDENTIFIER_BITWISE; } // If enabled, make room alias clickable if (_treatMatrixRoomAliasAsLink) { enabledMatrixIdsBitMask |= MXKTOOLS_ROOM_ALIAS_BITWISE; } // If enabled, make event id clickable if (_treatMatrixEventIdAsLink) { enabledMatrixIdsBitMask |= MXKTOOLS_EVENT_IDENTIFIER_BITWISE; } // If enabled, make group id clickable if (_treatMatrixGroupIdAsLink) { enabledMatrixIdsBitMask |= MXKTOOLS_GROUP_IDENTIFIER_BITWISE; } return [MXKTools createLinksInAttributedString:attributedString forEnabledMatrixIds:enabledMatrixIdsBitMask]; } - (NSAttributedString *)renderString:(NSString *)string withPrefix:(NSString *)prefix forEvent:(MXEvent *)event { NSMutableAttributedString *str; if (prefix) { str = [[NSMutableAttributedString alloc] initWithString:prefix]; // Apply the prefix font and color on the prefix NSRange prefixRange = NSMakeRange(0, prefix.length); [str addAttribute:NSForegroundColorAttributeName value:_prefixTextColor range:prefixRange]; [str addAttribute:NSFontAttributeName value:_prefixTextFont range:prefixRange]; // And append the string rendered according to event state [str appendAttributedString:[self renderString:string forEvent:event]]; return str; } else { // Use the legacy method return [self renderString:string forEvent:event]; } } - (void)setDefaultCSS:(NSString*)defaultCSS { // Make sure we mark HTML blockquote blocks for later computation _defaultCSS = [NSString stringWithFormat:@"%@%@", [MXKTools cssToMarkBlockquotes], defaultCSS]; dtCSS = [[DTCSSStylesheet alloc] initWithStyleBlock:_defaultCSS]; } #pragma mark - MXRoomSummaryUpdating - (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withStateEvents:(NSArray*)stateEvents roomState:(MXRoomState *)roomState { // We build strings containing the sender displayname (ex: "Bob: Hello!") // If a sender changes his displayname, we need to update the lastMessage. MXRoomLastMessage *lastMessage; for (MXEvent *event in stateEvents) { if (event.isUserProfileChange) { if (!lastMessage) { // Load lastMessageEvent on demand to save I/O lastMessage = summary.lastMessage; } if ([event.sender isEqualToString:lastMessage.sender]) { // The last message must be recomputed [summary resetLastMessage:nil failure:nil commit:YES]; break; } } else if (event.eventType == MXEventTypeRoomJoinRules) { summary.joinRule = roomState.joinRule; } } return [defaultRoomSummaryUpdater session:session updateRoomSummary:summary withStateEvents:stateEvents roomState:roomState]; } - (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withLastEvent:(MXEvent *)event eventState:(MXRoomState *)eventState roomState:(MXRoomState *)roomState { // Use the default updater as first pass MXRoomLastMessage *currentlastMessage = summary.lastMessage; BOOL updated = [defaultRoomSummaryUpdater session:session updateRoomSummary:summary withLastEvent:event eventState:eventState roomState:roomState]; if (updated) { // Then customise // Compute the text message // Note that we use the current room state (roomState) because when we display // users displaynames, we want current displaynames MXKEventFormatterError error; NSString *lastMessageString = [self stringFromEvent:event withRoomState:roomState error:&error]; if (0 == lastMessageString.length) { // @TODO: there is a conflict with what [defaultRoomSummaryUpdater updateRoomSummary] did :/ updated = NO; // Restore the previous lastMessageEvent [summary updateLastMessage:currentlastMessage]; } else { summary.lastMessage.text = lastMessageString; if (summary.lastMessage.others == nil) { summary.lastMessage.others = [NSMutableDictionary dictionary]; } // Store the potential error summary.lastMessage.others[@"mxkEventFormatterError"] = @(error); summary.lastMessage.others[@"lastEventDate"] = [self dateStringFromEvent:event withTime:YES]; // Check whether the sender name has to be added NSString *prefix = nil; if (event.eventType == MXEventTypeRoomMessage) { NSString *msgtype = event.content[@"msgtype"]; if ([msgtype isEqualToString:kMXMessageTypeEmote] == NO) { NSString *senderDisplayName = [self senderDisplayNameForEvent:event withRoomState:roomState]; prefix = [NSString stringWithFormat:@"%@: ", senderDisplayName]; } } else if (event.eventType == MXEventTypeSticker) { NSString *senderDisplayName = [self senderDisplayNameForEvent:event withRoomState:roomState]; prefix = [NSString stringWithFormat:@"%@: ", senderDisplayName]; } // Compute the attribute text message summary.lastMessage.attributedText = [self renderString:summary.lastMessage.text withPrefix:prefix forEvent:event]; } } return updated; } - (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withServerRoomSummary:(MXRoomSyncSummary *)serverRoomSummary roomState:(MXRoomState *)roomState { return [defaultRoomSummaryUpdater session:session updateRoomSummary:summary withServerRoomSummary:serverRoomSummary roomState:roomState]; } #pragma mark - Conversion private methods /** Get the text color to use according to the event state. @param event the event. @return the text color. */ - (UIColor*)textColorForEvent:(MXEvent*)event { // Select the text color UIColor *textColor; // Check whether an error occurred during event formatting. if (event.mxkEventFormatterError != MXKEventFormatterErrorNone) { textColor = _errorTextColor; } // Check whether the message is highlighted. else if (event.mxkIsHighlighted || (event.isInThread && ![event.sender isEqualToString:mxSession.myUserId])) { textColor = _bingTextColor; } else { // Consider here the sending state of the event, and the property `isForSubtitle`. switch (event.sentState) { case MXEventSentStateSent: if (_isForSubtitle) { textColor = _subTitleTextColor; } else { textColor = _defaultTextColor; } break; case MXEventSentStateEncrypting: textColor = _encryptingTextColor; break; case MXEventSentStatePreparing: case MXEventSentStateUploading: case MXEventSentStateSending: textColor = _sendingTextColor; break; case MXEventSentStateFailed: textColor = _errorTextColor; break; default: if (_isForSubtitle) { textColor = _subTitleTextColor; } else { textColor = _defaultTextColor; } break; } } return textColor; } /** Get the text font to use according to the event state. @param event the event. @return the text font. */ - (UIFont*)fontForEvent:(MXEvent*)event { // Select text font UIFont *font = _defaultTextFont; if (event.isState) { font = _stateEventTextFont; } else if (event.eventType == MXEventTypeCallInvite || event.eventType == MXEventTypeCallAnswer || event.eventType == MXEventTypeCallHangup) { font = _callNoticesTextFont; } else if (event.mxkIsHighlighted || (event.isInThread && ![event.sender isEqualToString:mxSession.myUserId])) { font = _bingTextFont; } else if (event.eventType == MXEventTypeRoomEncrypted) { font = _encryptedMessagesTextFont; } else if (!_isForSubtitle && event.eventType == MXEventTypeRoomMessage && (_emojiOnlyTextFont || _singleEmojiTextFont)) { NSString *message; MXJSONModelSetString(message, event.content[@"body"]); if (_emojiOnlyTextFont && [MXKTools isEmojiOnlyString:message]) { font = _emojiOnlyTextFont; } else if (_singleEmojiTextFont && [MXKTools isSingleEmojiString:message]) { font = _singleEmojiTextFont; } } return font; } #pragma mark - Conversion tools - (NSString *)htmlStringFromMarkdownString:(NSString *)markdownString { NSString *htmlString = [_markdownToHTMLRenderer renderToHTMLWithMarkdown:markdownString]; // Strip off the trailing newline, if it exists. if ([htmlString hasSuffix:@"\n"]) { htmlString = [htmlString substringToIndex:htmlString.length - 1]; } // Strip start and end tags else you get 'orrible spacing. // But only do this if it's a single paragraph we're dealing with, // otherwise we'll produce some garbage (`something
another`). if ([htmlString hasPrefix:@"
"] && [htmlString hasSuffix:@"
"]) { NSArray *components = [htmlString componentsSeparatedByString:@""]; NSUInteger paragrapsCount = components.count - 1; if (paragrapsCount == 1) { htmlString = [htmlString substringFromIndex:3]; htmlString = [htmlString substringToIndex:htmlString.length - 4]; } } return htmlString; } #pragma mark - Timestamp formatting - (NSString*)dateStringFromDate:(NSDate *)date withTime:(BOOL)time { // Get first date string without time (if a date format is defined, else only time string is returned) NSString *dateString = nil; if (dateFormatter.dateFormat) { dateString = [dateFormatter stringFromDate:date]; } if (time) { NSString *timeString = [self timeStringFromDate:date]; if (dateString.length) { // Add time string dateString = [NSString stringWithFormat:@"%@ %@", dateString, timeString]; } else { dateString = timeString; } } return dateString; } - (NSString*)dateStringFromTimestamp:(uint64_t)timestamp withTime:(BOOL)time { NSDate *date = [NSDate dateWithTimeIntervalSince1970:timestamp / 1000]; return [self dateStringFromDate:date withTime:time]; } - (NSString*)dateStringFromEvent:(MXEvent *)event withTime:(BOOL)time { if (event.originServerTs != kMXUndefinedTimestamp) { return [self dateStringFromTimestamp:event.originServerTs withTime:time]; } return nil; } - (NSString*)timeStringFromDate:(NSDate *)date { NSString *timeString = [timeFormatter stringFromDate:date]; return timeString.lowercaseString; } @end