Files
bundesmessenger-ios/Riot/Utils/EventFormatter.m
T
ismailgulek d572d54a0f Release 1.9.12 (#7081)
* Update voice broadcast tiles UI (#6965)

* Translated using Weblate (German)

Currently translated at 100.0% (2307 of 2307 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* speeding the animation a bit

* tests and identifier improvements

* fix

* changelog

* removed unused code

* Avoid unnecessary send state request (#6970)

* comment

* Curate MXCrypto protocol methods

* Add voice broadcast initial state in bubble data (#6972)

- Add voice broadcast initial state in bubble data
- Remove the local record after sending

* Voice Broadcast: log and block unexpected state change

* Sing out bottom bar

* new line

* Enable WYSIWYG plain text support

* Remove change on Apple swift-collections revision

* removed RiotSettings a non RiotSwiftUI reference from the ViewState code

* fixed a test

* Complete MXCryptoV2 implementation

* Multi session logut

* Switch the CI to code 14 and the iOS 14 simulator, fix UI tests

* Fixes #6987 - Prevent ZXing from unnecessarily requesting camera access

* Fixes #6988 - Prevent actor switching when tearing down the rendezvous

* Separator fix

* Removed warnings

* add Z-Labs tag or rich text editor and update to the new label naming

* changelog

* Hide old sessions list when the new dm is enabled

* Add changelog.d file

* Sing out filtering

* Avoid simultaneous state changes (#6986)

* Improve kebab menu in UserSessionOverview

* Add UI tests

* Add changelog.d file

* No customization for emptycell (#7000)

* PSG-976 Exclude current session from security recommendations and other sessions

* Padding fix

* Fixed unit tests

* Add empty onLearnMoreAction closure

* Add InfoView skeleton

* Add UserSessionOverviewViewBindings

* Style info view

* Add bottom sheet modifier

* Localise content

* Add inactive sessions copy

* Fix bug in InlineTextButton

* Improve UserSessionCardView

* Add “learn more” button in UserOtherSessions

* Show bottom sheet in user other sessions

* Show rename info alert

* Refine UX

* Add iOS 15- fallback

* Refine InfoView

* Add UI tests

* Improve UserOtherSessionsUITests

* Improve InlineTextButton API

* Add changelod.d file

* Fix failing UTs

* Hide keyboard in UserSessionName

* Add .viewSessionInfo view action

* Voice Broadcast - BugFix - send the last chunk (#7002)

* Voice Broadcast - BugFix - send the last chunk with the right sequence number

- we reset now and teardown the service only after the last chunk is sent

* updated package + tests

* change log

* Bug Fix : Crash if the room has avatar and voice broadcast tiles

* Add MVVM-C for InfoSheet

* improving UI tests for slow CI

* removing comment

* test improvements for slow ci

* Show bottom sheet in other sessions screen

* Show bottom sheet in rename session screen

* Delete bottom sheet modifier

* Show rename sheet

* UI and unit tests

* Refresh fix

* Changelog

* Add InfoSheet SwiftUI preview

* simplify the test to make it pass on the CI

* Fix memory leak

* Cleanup UI tests

* improving tests for the CI

* Fixed IRC-style message and commands support in Rich text editor

* tests updated for the CI

* test improvements

* removing a test that can't pass on the CI due to its speed

* Changelog

* CryptoV2 changes

* Display crypto version

* Voice broadcast - Disable the sleep mode during the recording until we are able to handle it

Currently go to "sleep mode" pauses the voice broadcast recording

* Add issue automation for the VoIP team

* Renamed sign out to logout

* Renamed sign out to logout

* Renamed sign out to logout

* Sign out of all other sessions

* Fix typo in issue automation

* Fixed unit tests

* Translations update from Weblate (#7017)

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2311 of 2311 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (2311 of 2311 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/

* Translated using Weblate (German)

Currently translated at 100.0% (2311 of 2311 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (German)

Currently translated at 100.0% (2311 of 2311 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2311 of 2311 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/

Co-authored-by: lvre <7uu3qrbvm@relay.firefox.com>
Co-authored-by: Priit Jõerüüt <riot@joeruut.com>
Co-authored-by: Vri <element@vrifox.cc>
Co-authored-by: Johan Smits <johan@smitsmail.net>

* Prepare for new sprint

* Prepare for new sprint

* Threads: added support to read receipts (MSC3771)

- Update after review

* Threads: added support to notifications count (MSC3773)

* Update RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift

Co-authored-by: aringenbach <80891108+aringenbach@users.noreply.github.com>

* Update RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift

Co-authored-by: aringenbach <80891108+aringenbach@users.noreply.github.com>

* Comment fix

* the test may fail on CI without blocking the task/check

* tests may fail on CI

* test improvement

* test may fail on CI

* Hide push toggles for http pushers when there is no server support

* changelog

* Code review fixes

* Threads: added support to read receipts (MSC3771)

- Update after review

* Synchronise composer and toolbar resizing animation duration

* Add kResizeComposerAnimationDuration constant description

* fix for 6946

* Threads: add support to labs flag for read receipts

* Cleanup

* Code review fixes, created DestructiveButton

* Update issue automation

Stop using deprecated ProjectNext API in favour of the new ProjectV2 one

* Update PR automation

Stop using deprecated ProjectNext API in favour of the new ProjectV2 one

* Code review fixes

* Map location info

* Map location info

* Add location feature in UserSessionsOverview

* Add “show location” feature in other sessions list

* Add “show location“ feature in session overview

* Fix Package.resolved

* Cleanup merge leftovers

* Cleanup code

* Cleanup

* Add show/hide ip persistency

* Add location info in UserOtherSessions

* Refine settings logic

* Mock settings in UserSessionsOverviewViewModel

* Add settings service in UserOtherSessionsViewModel

* Inject setting service in UserSessionOverviewViewModel

* Add changelog.d file

* Fix UTs

* Cleanup merge leftovers

* Add animations

* Fix failing test

* Amend title font

* Amend copies

* Device Manager: Session list item is not tappable everywhere

* changelog

* Threads notification count in main timeline including un participated threads

* Changed title and body

* Removed "Do not ask again" button

* Remove indication about plain text mode coming soon

* Prevent `Unable to activate constraint with anchors .. because they have no common ancestor.` crashes. Only link toasts to the top safe area instead of the navigation controller

* Revert "Replace attributed string height calculation with a more reliable implementation"

This reverts commit 81773cd1e515cc391c1f21b499f61141cb03c810.

* Revert "Fix timeline items text height calculation"

This reverts commit 8f9eddee501702de84192316bd5b2ff9512d681a.

* Revert "Fixes vector-im/element-ios/issues/6441 - Incorrect timeline item text height calculation (#6679)"

This reverts commit 405c2d8e324c08c1a40e037aeb3c54e93f30bc9f.

* Fixes vector-im/element-ios/issues/6441 - Incorrect timeline item text height calculation

* Prepare for new sprint

* Refine bottom sheet layout

* updated pod

* changelog

* Switch to using an API key for interactions with AppStoreConnect while on CI; update fastlane and dependencies

* Rich-text editor: Fix text formatting enabled inconsistent state

* Labs: Rich-text editor - Fix text formatting switch losing the current content of the composer

* Re-order View computed properties and move to private mark

* Add intrinsic sized bottom sheet

* Snooze controller

* Changelog

* Fix composer view model tests

* Rich-text editor: enable translations between Markdown and HTML when toggling text formatting

* Force a layout on the room bubble cell messageTextView to get a correct frame

* Move Move UserAgentParserTests

* Add UserSessionDetailsUITests

* Improve UserSessionNameUITests

* Cleanup tests

* Improve UserSessionNameViewModelTests

* Test empty state for UserOtherSessions

* Fix typo

* Cleanup unused code

* Add changelog.d file

* Threads: removed "unread_thread_notifications" from sync filters for server that doesn't support MSC3773

* Remove 10s wait on failed initial sync

* Threads: removed "unread_thread_notifications" from sync filters for server that doesn't support MSC3773

- Update after review

* Revert "Device Manager: Session list item is not tappable everywhere"

This reverts commit e6367cba4c8f0cb2cdfe5e3381dcbb7bc0f94c52.

* Fixup session list item is not tappable everywhere

* Fix accessibility id in UserOtherSessions

* Poll not usable after logging out and back in

* Changelog

* Removed init

* voice dictation now works

* plain text

* Add voice broadcast slider (#7010)

* Display number of unread messages above threads button

* Translations update from Weblate (#7080)

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2311 of 2311 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/

* Translated using Weblate (German)

Currently translated at 100.0% (2311 of 2311 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2312 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/

* Translated using Weblate (German)

Currently translated at 100.0% (2312 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (Albanian)

Currently translated at 99.6% (2303 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2312 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2312 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (2312 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (2312 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/

* Translated using Weblate (Slovak)

Currently translated at 100.0% (2312 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/

* Translated using Weblate (Italian)

Currently translated at 100.0% (2312 of 2312 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/

* Translated using Weblate (German)

Currently translated at 100.0% (2313 of 2313 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2313 of 2313 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (2313 of 2313 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/nl/

* Translated using Weblate (German)

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/

* Translated using Weblate (Italian)

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/

* Translated using Weblate (Slovak)

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2315 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/

* Translated using Weblate (Albanian)

Currently translated at 99.5% (2305 of 2315 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sq/

* Translated using Weblate (German)

Currently translated at 100.0% (2317 of 2317 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (Japanese)

Currently translated at 66.2% (1534 of 2317 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2317 of 2317 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2317 of 2317 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (2317 of 2317 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (2317 of 2317 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/

* Translated using Weblate (Slovak)

Currently translated at 100.0% (2317 of 2317 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/

* Translated using Weblate (Japanese)

Currently translated at 66.2% (1534 of 2317 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ja/

* Translated using Weblate (German)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/

* Translated using Weblate (German)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/uk/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/

* Translated using Weblate (Slovak)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sk/

* Translated using Weblate (Italian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/it/

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/hu/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (2326 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/id/

* Translated using Weblate (French)

Currently translated at 97.6% (2272 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/fr/

* Translated using Weblate (Russian)

Currently translated at 80.5% (1873 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ru/

* Translated using Weblate (Russian)

Currently translated at 80.7% (1879 of 2326 strings)

Translation: Element iOS/Element iOS
Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/ru/

Co-authored-by: Roel ter Maat <roel.termaat@nedap.com>
Co-authored-by: Vri <element@vrifox.cc>
Co-authored-by: Besnik Bleta <besnik@programeshqip.org>
Co-authored-by: lvre <7uu3qrbvm@relay.firefox.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Priit Jõerüüt <riot@joeruut.com>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Jozef Gaal <preklady@mayday.sk>
Co-authored-by: random <dictionary@tutamail.com>
Co-authored-by: Szimszon <github@oregpreshaz.eu>
Co-authored-by: Suguru Hirahara <ovestekona@protonmail.com>
Co-authored-by: Thibault Martin <mail@thibaultmart.in>
Co-authored-by: Platon Terekhov <ockenfels_vevent@aleeas.com>

* changelog.d: Upgrade MatrixSDK version ([v0.24.3](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.24.3)).

* version++

Co-authored-by: giomfo <gforet@matrix.org>
Co-authored-by: Yoan Pintas <y.pintas@gmail.com>
Co-authored-by: Vri <element@vrifox.cc>
Co-authored-by: Anderas <andyuhnak@gmail.com>
Co-authored-by: Mauro Romito <mauro.romito@element.io>
Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com>
Co-authored-by: Giom Foret <giom@matrix.org>
Co-authored-by: Aleksandrs Proskurins <paleksandrs@gmail.com>
Co-authored-by: aringenbach <arnaudr@element.io>
Co-authored-by: manuroe <manuroe@users.noreply.github.com>
Co-authored-by: David Langley <langley.dave@gmail.com>
Co-authored-by: Stefan Ceriu <stefanc@matrix.org>
Co-authored-by: Alfonso Grillo <alfogrillo@gmail.com>
Co-authored-by: Alfonso Grillo <alfogrillo@element.io>
Co-authored-by: Kat Gerasimova <ekaterinag@element.io>
Co-authored-by: Element Translate Bot <admin@riot.im>
Co-authored-by: lvre <7uu3qrbvm@relay.firefox.com>
Co-authored-by: Priit Jõerüüt <riot@joeruut.com>
Co-authored-by: Johan Smits <johan@smitsmail.net>
Co-authored-by: gulekismail <ismailgulek0@gmail.com>
Co-authored-by: Gil Eluard <gile@element.io>
Co-authored-by: Aleksandrs Proskurins <aleksandrsp@element.io>
Co-authored-by: aringenbach <80891108+aringenbach@users.noreply.github.com>
Co-authored-by: Stefan Ceriu <stefan.ceriu@gmail.com>
Co-authored-by: Roel ter Maat <roel.termaat@nedap.com>
Co-authored-by: Besnik Bleta <besnik@programeshqip.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Jozef Gaal <preklady@mayday.sk>
Co-authored-by: random <dictionary@tutamail.com>
Co-authored-by: Szimszon <github@oregpreshaz.eu>
Co-authored-by: Suguru Hirahara <ovestekona@protonmail.com>
Co-authored-by: Thibault Martin <mail@thibaultmart.in>
Co-authored-by: Platon Terekhov <ockenfels_vevent@aleeas.com>
2022-11-15 16:40:36 +03:00

737 lines
33 KiB
Objective-C

/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "EventFormatter.h"
#import "ThemeService.h"
#import "GeneratedInterface-Swift.h"
#import "WidgetManager.h"
#import "MXDecryptionResult.h"
#import "DecryptionFailureTracker.h"
#import "EventFormatter+DTCoreTextFix.h"
#import <MatrixSDK/MatrixSDK.h>
#pragma mark - Constants definitions
NSString *const EventFormatterOnReRequestKeysLinkAction = @"EventFormatterOnReRequestKeysLinkAction";
NSString *const EventFormatterLinkActionSeparator = @"/";
NSString *const EventFormatterEditedEventLinkAction = @"EventFormatterEditedEventLinkAction";
NSString *const FunctionalMembersStateEventType = @"io.element.functional_members";
NSString *const FunctionalMembersServiceMembersKey = @"service_members";
static NSString *const kEventFormatterTimeFormat = @"HH:mm";
@interface EventFormatter ()
{
/**
The calendar used to retrieve the today date.
*/
NSCalendar *calendar;
}
@end
@implementation EventFormatter
+ (void)load
{
[self fixDTCoreTextFont];
}
- (void)initDateTimeFormatters
{
[super initDateTimeFormatters];
timeFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
[timeFormatter setDateFormat:kEventFormatterTimeFormat];
}
- (NSString *)stringFromEvent:(MXEvent *)event
withRoomState:(MXRoomState *)roomState
andLatestRoomState:(MXRoomState *)latestRoomState
error:(MXKEventFormatterError *)error
{
NSString *stringFromEvent;
NSAttributedString *attributedStringFromEvent = [self attributedStringFromEvent:event
withRoomState:roomState
andLatestRoomState:latestRoomState
displayPills:NO
error:error];
if (*error == MXKEventFormatterErrorNone)
{
stringFromEvent = attributedStringFromEvent.string;
}
return stringFromEvent;
}
- (NSAttributedString *)attributedStringFromEvent:(MXEvent *)event
withRoomState:(MXRoomState *)roomState
andLatestRoomState:(MXRoomState *)latestRoomState
displayPills:(BOOL)displayPills
error:(MXKEventFormatterError *)error
{
NSAttributedString *string = [self unsafeAttributedStringFromEvent:event
withRoomState:roomState
andLatestRoomState:latestRoomState
error:error];
if (!string)
{
MXLogDebug(@"[EventFormatter]: No attributed string for event: %@, type: %@, msgtype: %@, has room state: %d, members: %lu, error: %lu",
event.eventId,
event.type,
event.content[@"msgtype"],
roomState != nil,
roomState.membersCount.members,
*error);
// If we cannot create attributed string, but the message is nevertheless meant for display, show generic error
// instead of a missing message on a timeline.
if ([self shouldDisplayEvent:event]) {
MXLogErrorDetails(@"[EventFormatter]: Missing attributed string for message event", @{
@"event_id": event.eventId ?: @"unknown"
});
string = [[NSAttributedString alloc] initWithString:[VectorL10n noticeErrorUnformattableEvent] attributes:@{
NSFontAttributeName: [self encryptedMessagesTextFont]
}];
}
}
if (@available(iOS 15.0, *))
{
if (displayPills && roomState && [self shouldDisplayEvent:event])
{
string = [PillsFormatter insertPillsIn:string
withSession:mxSession
eventFormatter:self
event:event
roomState:roomState
andLatestRoomState:latestRoomState
isEditMode:NO];
}
}
return string;
}
- (NSAttributedString *)attributedStringFromEvent:(MXEvent *)event
withRoomState:(MXRoomState *)roomState
andLatestRoomState:(MXRoomState *)latestRoomState
error:(MXKEventFormatterError *)error
{
return [self attributedStringFromEvent:event
withRoomState:roomState
andLatestRoomState:latestRoomState
displayPills:YES
error:error];
}
- (BOOL)shouldDisplayEvent:(MXEvent *)event {
return event.eventType == MXEventTypeRoomMessage
&& !event.isEditEvent
&& !event.isRedactedEvent;
}
// The attributed string can fail to be created for a number of reasons, and the size of the function (as well as super's implementation) makes
// it impossible to catch all the `return nil` and failure states.
// To make catching of missing strings reliable (and not place that burden on callers), we use private `unsafeAttributedStringFromEvent` method
// which is called by the public `attributedStringFromEvent`, and which also handles the catch-all missing message.
- (NSAttributedString *)unsafeAttributedStringFromEvent:(MXEvent *)event
withRoomState:(MXRoomState *)roomState
andLatestRoomState:(MXRoomState *)latestRoomState
error:(MXKEventFormatterError *)error
{
if (event.isRedactedEvent)
{
if (event.eventType == MXEventTypeReaction)
{
// do not show redacted reactions in the timeline
return nil;
}
// Check whether the event is a thread root or redacted information is required
if ((RiotSettings.shared.enableThreads && [mxSession.threadingService isEventThreadRoot:event])
|| self.settings.showRedactionsInRoomHistory)
{
NSAttributedString *result = [self redactedMessageReplacementAttributedString];
if (error)
{
*error = MXKEventFormatterErrorNone;
}
return result;
}
}
BOOL isEventSenderMyUser = [event.sender isEqualToString:mxSession.myUserId];
if (event.eventType == MXEventTypeCustom) {
// Build strings for widget events
if ([event.type isEqualToString:kWidgetMatrixEventTypeString]
|| [event.type isEqualToString:kWidgetModularEventTypeString])
{
NSString *displayText;
Widget *widget = [[Widget alloc] initWithWidgetEvent:event inMatrixSession:mxSession];
if (widget)
{
// Prepare the display name of the sender
NSString *senderDisplayName = roomState ? [self senderDisplayNameForEvent:event withRoomState:roomState] : event.sender;
if (widget.isActive)
{
if ([widget.type isEqualToString:kWidgetTypeJitsiV1]
|| [widget.type isEqualToString:kWidgetTypeJitsiV2])
{
// This is an alive jitsi widget
if (isEventSenderMyUser)
{
displayText = [VectorL10n eventFormatterJitsiWidgetAddedByYou];
}
else
{
displayText = [VectorL10n eventFormatterJitsiWidgetAdded:senderDisplayName];
}
}
else
{
if (isEventSenderMyUser)
{
displayText = [VectorL10n eventFormatterWidgetAddedByYou:(widget.name ? widget.name : widget.type)];
}
else
{
displayText = [VectorL10n eventFormatterWidgetAdded:(widget.name ? widget.name : widget.type) :senderDisplayName];
}
}
}
else
{
// This is a closed widget
// Check if it corresponds to a jitsi widget by looking at other state events for
// this jitsi widget (widget id = event.stateKey).
// Get all widgets state events in the room
NSMutableArray<MXEvent*> *widgetStateEvents = [NSMutableArray arrayWithArray:[roomState stateEventsWithType:kWidgetMatrixEventTypeString]];
[widgetStateEvents addObjectsFromArray:[roomState stateEventsWithType:kWidgetModularEventTypeString]];
for (MXEvent *widgetStateEvent in widgetStateEvents)
{
if ([widgetStateEvent.stateKey isEqualToString:widget.widgetId])
{
Widget *activeWidget = [[Widget alloc] initWithWidgetEvent:widgetStateEvent inMatrixSession:mxSession];
if (activeWidget.isActive)
{
if ([activeWidget.type isEqualToString:kWidgetTypeJitsiV1]
|| [activeWidget.type isEqualToString:kWidgetTypeJitsiV2])
{
// This was a jitsi widget
return nil;
}
else
{
if (isEventSenderMyUser)
{
displayText = [VectorL10n eventFormatterWidgetRemovedByYou:(activeWidget.name ? activeWidget.name : activeWidget.type)];
}
else
{
displayText = [VectorL10n eventFormatterWidgetRemoved:(activeWidget.name ? activeWidget.name : activeWidget.type) :senderDisplayName];
}
}
break;
}
}
}
}
}
if (displayText)
{
if (error)
{
*error = MXKEventFormatterErrorNone;
}
// Build the attributed string with the right font and color for the events
return [self renderString:displayText forEvent:event];
}
} else if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) {
// do not show voice broadcast info in the timeline
return nil;
}
}
switch (event.eventType)
{
case MXEventTypeRoomCreate:
{
MXRoomCreateContent *createContent = [MXRoomCreateContent modelFromJSON:event.content];
NSString *roomPredecessorId = createContent.roomPredecessorInfo.roomId;
if (roomPredecessorId)
{
return [self roomCreatePredecessorAttributedStringWithPredecessorRoomId:roomPredecessorId];
}
else
{
NSAttributedString *string = [super attributedStringFromEvent:event
withRoomState:roomState
andLatestRoomState:latestRoomState
error:error];
NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithString:@"· "];
[result appendAttributedString:string];
return result;
}
}
break;
case MXEventTypeCallCandidates:
case MXEventTypeCallSelectAnswer:
case MXEventTypeCallNegotiate:
case MXEventTypeCallReplaces:
case MXEventTypeCallRejectReplacement:
// Do not show call events except invite and reject in timeline
return nil;
case MXEventTypeCallInvite:
{
MXCallInviteEventContent *content = [MXCallInviteEventContent modelFromJSON:event.content];
MXCall *call = [mxSession.callManager callWithCallId:content.callId];
if (call && call.isIncoming && call.state == MXCallStateRinging)
{
// incoming call UI will be handled by CallKit (or incoming call screen if CallKit disabled)
// do not show a bubble for this case
return nil;
}
}
break;
case MXEventTypeKeyVerificationCancel:
case MXEventTypeKeyVerificationDone:
// Make event types MXEventTypeKeyVerificationCancel and MXEventTypeKeyVerificationDone visible in timeline.
// TODO: Find another way to keep them visible and avoid instantiate empty NSMutableAttributedString.
return [NSMutableAttributedString new];
default:
break;
}
NSAttributedString *attributedString = [super attributedStringFromEvent:event
withRoomState:roomState
andLatestRoomState:latestRoomState
error:error];
if (event.sentState == MXEventSentStateSent
&& [event.decryptionError.domain isEqualToString:MXDecryptingErrorDomain])
{
// Track e2e failures
dispatch_async(dispatch_get_main_queue(), ^{
[[DecryptionFailureTracker sharedInstance] reportUnableToDecryptErrorForEvent:event withRoomState:roomState myUser:self->mxSession.myUser.userId];
});
if (event.decryptionError.code == MXDecryptingErrorUnknownInboundSessionIdCode)
{
// Append to the displayed error an attibuted string with a tappable link
// so that the user can try to fix the UTD
NSMutableAttributedString *attributedStringWithRerequestMessage = [attributedString mutableCopy];
[attributedStringWithRerequestMessage appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n"]];
NSString *linkActionString = [NSString stringWithFormat:@"%@%@%@", EventFormatterOnReRequestKeysLinkAction,
EventFormatterLinkActionSeparator,
event.eventId];
[attributedStringWithRerequestMessage appendAttributedString:
[[NSAttributedString alloc] initWithString:[VectorL10n eventFormatterRerequestKeysPart1Link]
attributes:@{
NSLinkAttributeName: linkActionString,
NSForegroundColorAttributeName: self.sendingTextColor,
NSFontAttributeName: self.encryptedMessagesTextFont
}]];
[attributedStringWithRerequestMessage appendAttributedString:
[[NSAttributedString alloc] initWithString:[VectorL10n eventFormatterRerequestKeysPart2]
attributes:@{
NSForegroundColorAttributeName: self.sendingTextColor,
NSFontAttributeName: self.encryptedMessagesTextFont
}]];
attributedString = attributedStringWithRerequestMessage;
}
}
else if (self.showEditionMention && event.contentHasBeenEdited)
{
NSMutableAttributedString *attributedStringWithEditMention = [attributedString mutableCopy];
NSString *linkActionString = [NSString stringWithFormat:@"%@%@%@", EventFormatterEditedEventLinkAction,
EventFormatterLinkActionSeparator,
event.eventId];
[attributedStringWithEditMention appendAttributedString:
[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@" %@", [VectorL10n eventFormatterMessageEditedMention]]
attributes:@{
NSLinkAttributeName: linkActionString,
// NOTE: Color is curretly overidden by UIText.tintColor as we use `NSLinkAttributeName`.
// If we use UITextView.linkTextAttributes to set link color we will also have the issue that color will be the same for all kind of links.
NSForegroundColorAttributeName: self.editionMentionTextColor,
NSFontAttributeName: self.editionMentionTextFont
}]];
attributedString = attributedStringWithEditMention;
}
return attributedString;
}
- (NSAttributedString*)attributedStringFromEvents:(NSArray<MXEvent*>*)events
withRoomState:(MXRoomState*)roomState
andLatestRoomState:(MXRoomState*)latestRoomState
error:(MXKEventFormatterError*)error
{
NSString *displayText;
if (events.count)
{
MXEvent *roomCreateEvent = [events filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"type == %@", kMXEventTypeStringRoomCreate]].firstObject;
MXEvent *callInviteEvent = [events filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"type == %@", kMXEventTypeStringCallInvite]].firstObject;
if (roomCreateEvent)
{
MXKEventFormatterError tmpError;
displayText = [super attributedStringFromEvent:roomCreateEvent
withRoomState:roomState
andLatestRoomState:latestRoomState
error:&tmpError].string;
NSAttributedString *rendered = [self renderString:displayText forEvent:roomCreateEvent];
NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithString:@"· "];
[result appendAttributedString:rendered];
[result setAttributes:@{
NSFontAttributeName: [UIFont systemFontOfSize:13],
NSForegroundColorAttributeName: ThemeService.shared.theme.textSecondaryColor
} range:NSMakeRange(0, result.length)];
// add one-char space
[result appendAttributedString:[[NSAttributedString alloc] initWithString:@" "]];
// add more link
NSAttributedString *linkMore = [[NSAttributedString alloc] initWithString:[VectorL10n more] attributes:@{
NSFontAttributeName: [UIFont systemFontOfSize:13],
NSForegroundColorAttributeName: ThemeService.shared.theme.tintColor
}];
[result appendAttributedString:linkMore];
return result;
}
else if (callInviteEvent)
{
// return a non-nil value
return [NSMutableAttributedString new];
}
else if (events[0].eventType == MXEventTypeRoomMember)
{
// This is a series for cells tagged with RoomBubbleCellDataTagMembership
// TODO: Build a complete summary like Riot-web
displayText = [VectorL10n eventFormatterMemberUpdates:events.count];
}
}
if (displayText)
{
// Build the attributed string with the right font and color for the events
return [self renderString:displayText forEvent:events[0]];
}
return [super attributedStringFromEvents:events
withRoomState:roomState
andLatestRoomState:latestRoomState
error:error];
}
- (instancetype)initWithMatrixSession:(MXSession *)matrixSession
{
self = [super initWithMatrixSession:matrixSession];
if (self)
{
calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
// Use the selected bg color to set the code block background color in the default CSS.
NSUInteger bgColor = [MXKTools rgbValueWithColor:ThemeService.shared.theme.selectedBackgroundColor];
self.defaultCSS = [NSString stringWithFormat:@" \
pre,code { \
background-color: #%06lX; \
display: inline; \
font-family: monospace; \
white-space: pre; \
-coretext-fontname: Menlo-Regular; \
font-size: small; \
} \
h1,h2 { \
font-size: 1.2em; \
}", (unsigned long)bgColor];
self.defaultTextColor = ThemeService.shared.theme.textPrimaryColor;
self.subTitleTextColor = ThemeService.shared.theme.textSecondaryColor;
self.prefixTextColor = ThemeService.shared.theme.textSecondaryColor;
self.bingTextColor = ThemeService.shared.theme.noticeColor;
self.encryptingTextColor = ThemeService.shared.theme.textPrimaryColor;
self.sendingTextColor = ThemeService.shared.theme.textPrimaryColor;
self.errorTextColor = ThemeService.shared.theme.textPrimaryColor;
self.showEditionMention = YES;
self.editionMentionTextColor = ThemeService.shared.theme.textSecondaryColor;
self.defaultTextFont = [UIFont systemFontOfSize:15];
self.prefixTextFont = [UIFont boldSystemFontOfSize:15];
self.bingTextFont = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium];
self.stateEventTextFont = [UIFont italicSystemFontOfSize:15];
self.callNoticesTextFont = [UIFont italicSystemFontOfSize:15];
self.encryptedMessagesTextFont = [UIFont italicSystemFontOfSize:15];
self.emojiOnlyTextFont = [UIFont systemFontOfSize:48];
self.editionMentionTextFont = [UIFont systemFontOfSize:12];
// Handle space and video room types, enables their display in the room list
defaultRoomSummaryUpdater.showRoomTypeStrings = @[
MXRoomTypeStringSpace,
MXRoomTypeStringVideo
];
}
return self;
}
- (NSDictionary*)stringAttributesForEventTimestamp
{
return @{
NSForegroundColorAttributeName : [UIColor lightGrayColor],
NSFontAttributeName: [UIFont systemFontOfSize:10]
};
}
#pragma mark event sender info
- (NSString*)senderAvatarUrlForEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState
{
// Override this method to ignore the identicons defined by default in matrix kit.
// Consider first the avatar url defined in provided room state (Note: this room state is supposed to not take the new event into account)
NSString *senderAvatarUrl = [roomState.members memberWithUserId:event.sender].avatarUrl;
// Check whether this avatar url is updated by the current event (This happens in case of new joined member)
NSString* membership = event.content[@"membership"];
NSString* eventAvatarUrl = event.content[@"avatar_url"];
NSString* prevEventAvatarUrl = event.prevContent[@"avatar_url"];
if (membership && [membership isEqualToString:@"join"] && [eventAvatarUrl length] && ![eventAvatarUrl isEqualToString:prevEventAvatarUrl])
{
// Use the actual avatar
senderAvatarUrl = eventAvatarUrl;
}
// We ignore non mxc avatar url (The identicons are removed here).
if (senderAvatarUrl && [senderAvatarUrl hasPrefix:kMXContentUriScheme] == NO)
{
senderAvatarUrl = nil;
}
return senderAvatarUrl;
}
#pragma mark - MXRoomSummaryUpdating
- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withStateEvents:(NSArray<MXEvent *> *)stateEvents roomState:(MXRoomState *)roomState
{
BOOL updated = [super session:session updateRoomSummary:summary withStateEvents:stateEvents roomState:roomState];
// Customisation for EMS Functional Members in direct rooms
if (BuildSettings.supportFunctionalMembers && summary.room.isDirect)
{
if ([self functionalMembersEventFromStateEvents:stateEvents])
{
MXLogDebug(@"[EventFormatter] The functional members event has been updated.")
// The stateEvents parameter contains state events that may change the room summary. If service members are found,
// it's likely that something changed. As they aren't stored, the only reliable check would be to compute the
// room name which we'll do twice more in updateRoomSummary:withServerRoomSummary:roomState: anyway.
//
// So return YES and let that happen there.
return YES;
}
}
return updated;
}
- (NSAttributedString *)redactedMessageReplacementAttributedString
{
UIFont *font = self.defaultTextFont;
UIColor *color = ThemeService.shared.theme.colors.secondaryContent;
NSString *string = [NSString stringWithFormat:@" %@", VectorL10n.eventFormatterMessageDeleted];
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string
attributes:@{
NSFontAttributeName: font,
NSForegroundColorAttributeName: color
}];
CGSize imageSize = CGSizeMake(20, 20);
NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
attachment.image = [[AssetImages.roomContextMenuDelete.image vc_resizedWith:imageSize] vc_tintedImageUsingColor:color];
attachment.bounds = CGRectMake(0, font.descender, imageSize.width, imageSize.height);
NSAttributedString *imageString = [NSAttributedString attributedStringWithAttachment:attachment];
NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithAttributedString:imageString];
[result appendAttributedString:attrString];
return result;
}
- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withServerRoomSummary:(MXRoomSyncSummary *)serverRoomSummary roomState:(MXRoomState *)roomState
{
BOOL updated = [super session:session updateRoomSummary:summary withServerRoomSummary:serverRoomSummary roomState:roomState];
// Customisation for EMS Functional Members in direct rooms
if (BuildSettings.supportFunctionalMembers && summary.room.isDirect)
{
MXEvent *functionalMembersEvent = [self functionalMembersEventFromStateEvents:roomState.stateEvents];
if (functionalMembersEvent)
{
MXLogDebug(@"[EventFormatter] Computing the room name and avatar excluding functional members.")
NSArray<NSString*> *serviceMemberIDs = functionalMembersEvent.content[FunctionalMembersServiceMembersKey] ?: @[];
updated |= [defaultRoomSummaryUpdater updateSummaryDisplayname:summary
session:session
withServerRoomSummary:serverRoomSummary
roomState:roomState
excludingUserIDs:serviceMemberIDs];
updated |= [defaultRoomSummaryUpdater updateSummaryAvatar:summary
session:session
withServerRoomSummary:serverRoomSummary
roomState:roomState
excludingUserIDs:serviceMemberIDs];
}
}
return updated;
}
/**
Gets the latest state event of type `io.element.functional_members` from the supplied array of state events.
Note: This function will be expensive on big rooms, recommended for use only on DMs.
@return An event of type `io.element.functional_members`, or nil if the event wasn't found.
*/
- (MXEvent *)functionalMembersEventFromStateEvents:(NSArray<MXEvent *> *)stateEvents
{
NSPredicate *functionalMembersPredicate = [NSPredicate predicateWithFormat:@"type == %@", FunctionalMembersStateEventType];
return [stateEvents filteredArrayUsingPredicate:functionalMembersPredicate].lastObject;
}
#pragma mark - Timestamp formatting
- (NSString*)dateStringFromDate:(NSDate *)date withTime:(BOOL)time
{
// Check the provided date
if (!date)
{
return nil;
}
// Retrieve today date at midnight
NSDate *today = [calendar startOfDayForDate:[NSDate date]];
NSTimeInterval interval = -[date timeIntervalSinceDate:today];
if (interval > 60*60*24*364)
{
[dateFormatter setDateFormat:@"MMM dd yyyy"];
// Ignore time information here
return [super dateStringFromDate:date withTime:NO];
}
else if (interval > 60*60*24*6)
{
[dateFormatter setDateFormat:@"MMM dd"];
// Ignore time information here
return [super dateStringFromDate:date withTime:NO];
}
else if (interval > 60*60*24)
{
if (time)
{
[dateFormatter setDateFormat:@"EEE"];
}
else
{
[dateFormatter setDateFormat:@"EEEE"];
}
return [super dateStringFromDate:date withTime:time];
}
else if (interval > 0)
{
if (time)
{
[dateFormatter setDateFormat:nil];
return [NSString stringWithFormat:@"%@ %@", [VectorL10n yesterday], [super dateStringFromDate:date withTime:YES]];
}
return [VectorL10n yesterday];
}
else if (interval > - 60*60*24)
{
if (time)
{
[dateFormatter setDateFormat:nil];
return [NSString stringWithFormat:@"%@", [super dateStringFromDate:date withTime:YES]];
}
return [VectorL10n today];
}
else
{
// Date in future
[dateFormatter setDateFormat:@"EEE MMM dd yyyy"];
return [super dateStringFromDate:date withTime:time];
}
}
#pragma mark - Room create predecessor
- (NSAttributedString*)roomCreatePredecessorAttributedStringWithPredecessorRoomId:(NSString*)predecessorRoomId
{
NSDictionary *roomPredecessorReasonAttributes = @{
NSFontAttributeName : self.defaultTextFont
};
NSDictionary *roomLinkAttributes = @{
NSFontAttributeName : self.defaultTextFont,
NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle)
};
NSMutableAttributedString *roomPredecessorAttributedString = [NSMutableAttributedString new];
NSString *roomPredecessorReasonString = [NSString stringWithFormat:@"%@\n", [VectorL10n roomPredecessorInformation]];
NSAttributedString *roomPredecessorReasonAttributedString = [[NSAttributedString alloc] initWithString:roomPredecessorReasonString attributes:roomPredecessorReasonAttributes];
NSString *predecessorRoomLinkString = [VectorL10n roomPredecessorLink];
NSAttributedString *predecessorRoomLinkAttributedString = [[NSAttributedString alloc] initWithString:predecessorRoomLinkString attributes:roomLinkAttributes];
[roomPredecessorAttributedString appendAttributedString:roomPredecessorReasonAttributedString];
[roomPredecessorAttributedString appendAttributedString:predecessorRoomLinkAttributedString];
NSRange wholeStringRange = NSMakeRange(0, roomPredecessorAttributedString.length);
[roomPredecessorAttributedString addAttribute:NSForegroundColorAttributeName value:self.defaultTextColor range:wholeStringRange];
return roomPredecessorAttributedString;
}
@end