Files
bundesmessenger-ios/Riot/Modules/Room/CellData/RoomBubbleCellData.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

1473 lines
52 KiB
Objective-C

/*
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 "RoomReactionsViewSizer.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<NSString* /* eventId */> *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 *)roomDataSource
{
self = [super initWithEvent:event andRoomState:roomState andRoomDataSource:roomDataSource];
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 = RoomBubbleCellDataTagRoomCreationIntro;
}
// 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 MXEventTypeBeaconInfo:
{
self.tag = RoomBubbleCellDataTagLiveLocation;
self.collapsable = NO;
self.collapsed = NO;
[self updateBeaconInfoSummaryWithId:event.eventId andEvent:event];
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;
}
}
else if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType])
{
VoiceBroadcastInfo *voiceBroadcastInfo = [VoiceBroadcastInfo modelFromJSON: event.content];
// Check if the state event corresponds to the beginning of a voice broadcast
if ([VoiceBroadcastInfo isStartedFor:voiceBroadcastInfo.state])
{
// Retrieve the most recent voice broadcast info.
MXEvent *lastVoiceBroadcastInfoEvent = [roomDataSource.roomState stateEventsWithType:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType].lastObject;
if (event.originServerTs > lastVoiceBroadcastInfoEvent.originServerTs)
{
lastVoiceBroadcastInfoEvent = event;
}
VoiceBroadcastInfo *lastVoiceBroadcastInfo = [VoiceBroadcastInfo modelFromJSON: lastVoiceBroadcastInfoEvent.content];
// Handle the specific case where the state event is a started voice broadcast (the voiceBroadcastId is the event id itself).
if (!lastVoiceBroadcastInfo.voiceBroadcastId)
{
lastVoiceBroadcastInfo.voiceBroadcastId = lastVoiceBroadcastInfoEvent.eventId;
}
// Check if the voice broadcast is still alive.
if ([lastVoiceBroadcastInfo.voiceBroadcastId isEqualToString:event.eventId] && ![VoiceBroadcastInfo isStoppedFor:lastVoiceBroadcastInfo.state])
{
// Check whether this broadcast is sent from the currrent session to display it with the recorder view or not.
if ([event.stateKey isEqualToString:self.mxSession.myUserId] &&
[voiceBroadcastInfo.deviceId isEqualToString:self.mxSession.myDeviceId])
{
self.tag = RoomBubbleCellDataTagVoiceBroadcastRecord;
}
else
{
self.tag = RoomBubbleCellDataTagVoiceBroadcastPlayback;
}
self.voiceBroadcastState = lastVoiceBroadcastInfo.state;
}
else
{
self.tag = RoomBubbleCellDataTagVoiceBroadcastPlayback;
self.voiceBroadcastState = VoiceBroadcastInfo.stoppedValue;
}
}
else
{
self.tag = RoomBubbleCellDataTagVoiceBroadcastNoDisplay;
if ([VoiceBroadcastInfo isStoppedFor:voiceBroadcastInfo.state])
{
// This state event corresponds to the end of a voice broadcast
// Force the tag of the potential cellData which corresponds to the started event to switch the display from recorder to listener
RoomBubbleCellData *bubbleData = [roomDataSource cellDataOfEventWithEventId:voiceBroadcastInfo.voiceBroadcastId];
bubbleData.tag = RoomBubbleCellDataTagVoiceBroadcastPlayback;
bubbleData.voiceBroadcastState = VoiceBroadcastInfo.stoppedValue;
}
}
self.collapsable = NO;
self.collapsed = NO;
break;
}
break;
}
case MXEventTypeRoomMessage:
{
if (event.location)
{
self.tag = RoomBubbleCellDataTagLocation;
self.collapsable = NO;
self.collapsed = NO;
}
else if (event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType])
{
self.tag = RoomBubbleCellDataTagVoiceBroadcastNoDisplay;
self.collapsable = NO;
self.collapsed = NO;
}
break;
}
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];
if (self.tag == RoomBubbleCellDataTagLiveLocation)
{
[self updateBeaconInfoSummaryWithId:eventId andEvent:event];
}
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
{
[self buildAttributedStringIfNeeded];
return attributedTextMessage;
}
- (NSAttributedString*)attributedTextMessageWithoutPositioningSpace
{
[self buildAttributedStringIfNeeded];
return attributedTextMessageWithoutPositioningSpace;
}
- (BOOL)hasNoDisplay
{
BOOL hasNoDisplay = YES;
switch (self.tag)
{
case RoomBubbleCellDataTagKeyVerificationNoDisplay:
hasNoDisplay = YES;
break;
case RoomBubbleCellDataTagRoomCreationIntro:
hasNoDisplay = NO;
break;
case RoomBubbleCellDataTagPoll:
if (!self.events.lastObject.isEditEvent)
{
hasNoDisplay = NO;
}
break;
case RoomBubbleCellDataTagLocation:
hasNoDisplay = NO;
break;
case RoomBubbleCellDataTagLiveLocation:
// Show the cell only if the summary exists
if (self.beaconInfoSummary)
{
hasNoDisplay = NO;
}
break;
case RoomBubbleCellDataTagVoiceBroadcastRecord:
case RoomBubbleCellDataTagVoiceBroadcastPlayback:
hasNoDisplay = NO;
break;
case RoomBubbleCellDataTagVoiceBroadcastNoDisplay:
break;
default:
hasNoDisplay = [super hasNoDisplay];
break;
}
return hasNoDisplay;
}
- (BOOL)hasThreadRoot
{
if (!RiotSettings.shared.enableThreads)
{
// do not consider this cell data if threads not enabled in the timeline
return NO;
}
if (roomDataSource.threadId)
{
// do not consider this cell data if in a thread view
return NO;
}
return super.hasThreadRoot;
}
- (BOOL)mergeWithBubbleCellData:(id<MXKRoomBubbleCellDataStoring>)bubbleCellData
{
RoomTimelineConfiguration *timelineConfiguration = [RoomTimelineConfiguration shared];
if (NO == [timelineConfiguration.currentStyle canMergeWithCellData:bubbleCellData into:self]) {
return NO;
}
return [super mergeWithBubbleCellData:bubbleCellData];
}
#pragma mark - Bubble collapsing
- (BOOL)collapseWith:(id<MXKRoomBubbleCellDataStoring>)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];
}
- (void)buildAttributedString
{
// 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)
{
NSAttributedString *attributedString = super.collapsedAttributedTextMessage;
self.attributedTextMessage = attributedString;
self.attributedTextMessageWithoutPositioningSpace = attributedString;
return;
}
NSMutableAttributedString *currentAttributedTextMsg;
NSMutableAttributedString *currentAttributedTextMsgWithoutVertSpace = [NSMutableAttributedString new];
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 = [componentString withTextColorAlpha:.2];
if (@available(iOS 15.0, *)) {
[PillsFormatter setPillAlpha:.2 inAttributedString:componentString];
}
}
else if (@available(iOS 15.0, *))
{
// PillTextAttachment are not created again every time, we have to set alpha back to standard if needed.
[PillsFormatter setPillAlpha:1.f 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];
[currentAttributedTextMsgWithoutVertSpace appendAttributedString:componentString];
}
else
{
// Init attributed string with the first text component
currentAttributedTextMsg = [[NSMutableAttributedString alloc] initWithAttributedString:componentString];
[currentAttributedTextMsgWithoutVertSpace appendAttributedString: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 = [componentString withTextColorAlpha:.2];
if (@available(iOS 15.0, *)) {
[PillsFormatter setPillAlpha:.2 inAttributedString:componentString];
}
}
else if (@available(iOS 15.0, *))
{
// PillTextAttachment are not created again every time, we have to set alpha back to standard if needed.
[PillsFormatter setPillAlpha:1.f 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];
[currentAttributedTextMsgWithoutVertSpace appendAttributedString:componentString];
}
}
// With bubbles the text is truncated with quote messages containing vertical border view
// Add horizontal space to fix the issue
if (self.displayFix & MXKRoomBubbleComponentDisplayFixHtmlBlockquote)
{
[currentAttributedTextMsgWithoutVertSpace appendString:@" "];
}
self.attributedTextMessage = currentAttributedTextMsg;
self.attributedTextMessageWithoutPositioningSpace = currentAttributedTextMsgWithoutVertSpace;
}
- (void)buildAttributedStringIfNeeded
{
@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 buildAttributedString];
});
}
else
{
[self buildAttributedString];
}
}
}
}
- (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 a thread root
additionalVerticalHeight+= [self threadSummaryViewHeightForEventId:eventId];
// Add vertical whitespace in case of from a thread
additionalVerticalHeight+= [self fromAThreadViewHeightForEventId: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 threadSummaryViewHeightForEventId:eventId];
height+= [self fromAThreadViewHeightForEventId: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)threadSummaryViewHeightForEventId:(NSString*)eventId
{
if (!RiotSettings.shared.enableThreads)
{
// do not show thread summary view if threads not enabled in the timeline
return 0;
}
if (roomDataSource.threadId)
{
// do not show thread summary view on threads
return 0;
}
NSInteger index = [self bubbleComponentIndexForEventId:eventId];
if (index == NSNotFound)
{
return 0;
}
MXKRoomBubbleComponent *component = self.bubbleComponents[index];
if (!component.thread)
{
// component is not a thread root
return 0;
}
return PlainRoomCellLayoutConstants.threadSummaryViewTopMargin +
[ThreadSummaryView contentViewHeightForThread:component.thread fitting:self.maxTextViewWidth];
}
- (CGFloat)fromAThreadViewHeightForEventId:(NSString*)eventId
{
if (!RiotSettings.shared.enableThreads)
{
// do not show from a thread view if threads not enabled
return 0;
}
if (roomDataSource.threadId)
{
// do not show from a thread view on threads
return 0;
}
NSInteger index = [self bubbleComponentIndexForEventId:eventId];
if (index == NSNotFound)
{
return 0;
}
MXKRoomBubbleComponent *component = self.bubbleComponents[index];
if (!component.event.isInThread)
{
// event is not in a thread
return 0;
}
return PlainRoomCellLayoutConstants.fromAThreadViewTopMargin +
[FromAThreadView contentViewHeightForEvent:component.event fitting:self.maxTextViewWidth];
}
- (CGFloat)urlPreviewHeightForEventId:(NSString*)eventId
{
MXKRoomBubbleComponent *component = [self bubbleComponentWithLinkForEventId:eventId];
if (!component.showURLPreview)
{
return 0;
}
return PlainRoomCellLayoutConstants.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 reactionsViewWidth = self.maxTextViewWidth - 4;
static RoomReactionsViewSizer *reactionsViewSizer;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
reactionsViewSizer = [RoomReactionsViewSizer new];
});
BOOL showAllReactions = [self.eventsToShowAllReactions containsObject:eventId];
RoomReactionsViewModel *viewModel = [[RoomReactionsViewModel alloc] initWithAggregatedReactions:aggregatedReactions eventId:eventId showAll:showAllReactions];
height = [reactionsViewSizer heightForViewModel:viewModel fittingWidth:reactionsViewWidth] + PlainRoomCellLayoutConstants.reactionsViewTopMargin;
}
return height;
}
- (CGFloat)readReceiptHeightForEventId:(NSString*)eventId
{
CGFloat height = 0;
if (self.readReceipts[eventId].count)
{
height = PlainRoomCellLayoutConstants.readReceiptsViewHeight + PlainRoomCellLayoutConstants.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<MXKRoomBubbleCellDataStoring>)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;
}
if (self.hasThreadRoot || bubbleCellData.hasThreadRoot)
{
// We do not want to merge events containing thread roots
return NO;
}
return [super hasSameSenderAsBubbleCellData:bubbleCellData];
}
- (BOOL)addEvent:(MXEvent*)event andRoomState:(MXRoomState*)roomState
{
if (self.hasThreadRoot)
{
// We don't want to add any events into this bubble data if it's a thread root
return NO;
}
RoomTimelineConfiguration *timelineConfiguration = [RoomTimelineConfiguration shared];
if (NO == [timelineConfiguration.currentStyle canAddEvent:event and:roomState to:self]) {
return NO;
}
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;
case RoomBubbleCellDataTagLiveLocation:
shouldAddEvent = NO;
break;
case RoomBubbleCellDataTagVoiceBroadcastRecord:
case RoomBubbleCellDataTagVoiceBroadcastPlayback:
case RoomBubbleCellDataTagVoiceBroadcastNoDisplay:
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;
}
} else if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) {
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<NSString *, NSString*> *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];
});
}];
}
- (void)updateBeaconInfoSummaryWithId:(NSString *)eventId andEvent:(MXEvent*)event
{
if (event.eventType != MXEventTypeBeaconInfo)
{
MXLogErrorDetails(@"[RoomBubbleCellData] Try to update beacon info summary with wrong event type", @{
@"event_id": eventId ?: @"unknown"
});
return;
}
id<MXBeaconInfoSummaryProtocol> beaconInfoSummary = [self.mxSession.aggregations.beaconAggregations beaconInfoSummaryFor:eventId inRoomWithId:self.roomId];
if (!beaconInfoSummary)
{
MXBeaconInfo *beaconInfo = [[MXBeaconInfo alloc] initWithMXEvent:event];
// A start beacon info event (isLive == true) should have an associated BeaconInfoSummary
if (beaconInfo && beaconInfo.isLive)
{
MXLogErrorDetails(@"[RoomBubbleCellData] No beacon info summary found for beacon info start event", @{
@"event_id": eventId ?: @"unknown"
});
}
}
self.beaconInfoSummary = beaconInfoSummary;
}
@end