mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Render Room and Message Pills (#3809)
* added a way to render the room and the message pills, but is WIP * permalinks now get converted into pills! * fixed an issue where room address mentions were not adding a URL properly but a string * updated tests * c * Revert "c" This reverts commit 5c80252fa23dba7e4d44f2a07fbf1e9500e37c82. * updated tests * more tests * created APIs to get a specific RoomSummary given the id or the alias * small mention builder improvement * pr suggestions
This commit is contained in:
@@ -193,6 +193,7 @@ private extension HomeScreenRoom {
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: nil,
|
||||
canonicalAlias: "#footest:somewhere.org",
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false)
|
||||
@@ -220,6 +221,7 @@ private extension HomeScreenRoom {
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: nil,
|
||||
canonicalAlias: alias,
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false)
|
||||
|
||||
@@ -157,6 +157,7 @@ private extension HomeScreenRoom {
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: nil,
|
||||
canonicalAlias: "#footest:somewhere.org",
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false)
|
||||
@@ -184,6 +185,7 @@ private extension HomeScreenRoom {
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: nil,
|
||||
canonicalAlias: alias,
|
||||
alternativeAliases: [],
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false)
|
||||
|
||||
+5
-2
@@ -19,6 +19,7 @@ struct MediaEventsTimelineScreenCoordinatorParameters {
|
||||
let emojiProvider: EmojiProviderProtocol
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
let timelineControllerFactory: TimelineControllerFactoryProtocol
|
||||
let clientProxy: ClientProxyProtocol
|
||||
}
|
||||
|
||||
enum MediaEventsTimelineScreenCoordinatorAction {
|
||||
@@ -49,7 +50,8 @@ final class MediaEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: parameters.emojiProvider,
|
||||
timelineControllerFactory: parameters.timelineControllerFactory)
|
||||
timelineControllerFactory: parameters.timelineControllerFactory,
|
||||
clientProxy: parameters.clientProxy)
|
||||
|
||||
let filesTimelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
|
||||
timelineController: parameters.filesTimelineController,
|
||||
@@ -61,7 +63,8 @@ final class MediaEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: parameters.emojiProvider,
|
||||
timelineControllerFactory: parameters.timelineControllerFactory)
|
||||
timelineControllerFactory: parameters.timelineControllerFactory,
|
||||
clientProxy: parameters.clientProxy)
|
||||
|
||||
viewModel = MediaEventsTimelineScreenViewModel(mediaTimelineViewModel: mediaTimelineViewModel,
|
||||
filesTimelineViewModel: filesTimelineViewModel,
|
||||
|
||||
+2
-1
@@ -274,6 +274,7 @@ struct MediaEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -17,6 +17,7 @@ struct PinnedEventsTimelineScreenCoordinatorParameters {
|
||||
let appMediator: AppMediatorProtocol
|
||||
let emojiProvider: EmojiProviderProtocol
|
||||
let timelineControllerFactory: TimelineControllerFactoryProtocol
|
||||
let clientProxy: ClientProxyProtocol
|
||||
}
|
||||
|
||||
enum PinnedEventsTimelineScreenCoordinatorAction {
|
||||
@@ -53,7 +54,8 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: parameters.emojiProvider,
|
||||
timelineControllerFactory: parameters.timelineControllerFactory)
|
||||
timelineControllerFactory: parameters.timelineControllerFactory,
|
||||
clientProxy: parameters.clientProxy)
|
||||
}
|
||||
|
||||
func start() {
|
||||
|
||||
+2
-1
@@ -99,7 +99,8 @@ struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
}()
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
@@ -86,7 +86,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
appSettings: parameters.appSettings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: parameters.emojiProvider,
|
||||
timelineControllerFactory: parameters.timelineControllerFactory)
|
||||
timelineControllerFactory: parameters.timelineControllerFactory,
|
||||
clientProxy: parameters.clientProxy)
|
||||
|
||||
wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight,
|
||||
maxCompressedHeight: ComposerConstant.maxHeight,
|
||||
|
||||
@@ -266,7 +266,8 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
|
||||
@@ -41,6 +41,7 @@ class TimelineInteractionHandler {
|
||||
private let emojiProvider: EmojiProviderProtocol
|
||||
private let timelineControllerFactory: TimelineControllerFactoryProtocol
|
||||
private let pollInteractionHandler: PollInteractionHandlerProtocol
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
|
||||
private let actionsSubject: PassthroughSubject<TimelineInteractionHandlerAction, Never> = .init()
|
||||
var actions: AnyPublisher<TimelineInteractionHandlerAction, Never> {
|
||||
@@ -66,7 +67,8 @@ class TimelineInteractionHandler {
|
||||
appSettings: AppSettings,
|
||||
analyticsService: AnalyticsService,
|
||||
emojiProvider: EmojiProviderProtocol,
|
||||
timelineControllerFactory: TimelineControllerFactoryProtocol) {
|
||||
timelineControllerFactory: TimelineControllerFactoryProtocol,
|
||||
clientProxy: ClientProxyProtocol) {
|
||||
self.roomProxy = roomProxy
|
||||
self.timelineController = timelineController
|
||||
self.mediaProvider = mediaProvider
|
||||
@@ -79,6 +81,7 @@ class TimelineInteractionHandler {
|
||||
self.analyticsService = analyticsService
|
||||
self.emojiProvider = emojiProvider
|
||||
self.timelineControllerFactory = timelineControllerFactory
|
||||
self.clientProxy = clientProxy
|
||||
pollInteractionHandler = PollInteractionHandler(analyticsService: analyticsService, roomProxy: roomProxy)
|
||||
}
|
||||
|
||||
@@ -580,7 +583,8 @@ class TimelineInteractionHandler {
|
||||
appSettings: appSettings,
|
||||
analyticsService: analyticsService,
|
||||
emojiProvider: emojiProvider,
|
||||
timelineControllerFactory: timelineControllerFactory)
|
||||
timelineControllerFactory: timelineControllerFactory,
|
||||
clientProxy: clientProxy)
|
||||
|
||||
return .displayMediaPreview(item: item, timelineViewModel: .new(timelineViewModel))
|
||||
} else {
|
||||
|
||||
@@ -114,6 +114,9 @@ struct TimelineViewState: BindableState {
|
||||
/// A closure providing the associated audio player state for an item in the timeline.
|
||||
var audioPlayerStateProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> AudioPlayerState?)?
|
||||
|
||||
/// A closure that updates the associated pill context
|
||||
var pillContextUpdater: (@MainActor (PillContext) -> Void)?
|
||||
|
||||
var emojiProvider: EmojiProviderProtocol
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
private let analyticsService: AnalyticsService
|
||||
private let emojiProvider: EmojiProviderProtocol
|
||||
private let timelineControllerFactory: TimelineControllerFactoryProtocol
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
|
||||
private let timelineInteractionHandler: TimelineInteractionHandler
|
||||
|
||||
@@ -55,7 +56,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
appSettings: AppSettings,
|
||||
analyticsService: AnalyticsService,
|
||||
emojiProvider: EmojiProviderProtocol,
|
||||
timelineControllerFactory: TimelineControllerFactoryProtocol) {
|
||||
timelineControllerFactory: TimelineControllerFactoryProtocol,
|
||||
clientProxy: ClientProxyProtocol) {
|
||||
self.timelineController = timelineController
|
||||
self.mediaProvider = mediaProvider
|
||||
self.mediaPlayerProvider = mediaPlayerProvider
|
||||
@@ -66,6 +68,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
self.appMediator = appMediator
|
||||
self.emojiProvider = emojiProvider
|
||||
self.timelineControllerFactory = timelineControllerFactory
|
||||
self.clientProxy = clientProxy
|
||||
|
||||
let voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider)
|
||||
|
||||
@@ -80,7 +83,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
appSettings: appSettings,
|
||||
analyticsService: analyticsService,
|
||||
emojiProvider: emojiProvider,
|
||||
timelineControllerFactory: timelineControllerFactory)
|
||||
timelineControllerFactory: timelineControllerFactory,
|
||||
clientProxy: clientProxy)
|
||||
|
||||
super.init(initialViewState: TimelineViewState(timelineKind: timelineController.timelineKind,
|
||||
roomID: roomProxy.id,
|
||||
@@ -113,6 +117,10 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
return self.timelineInteractionHandler.audioPlayerState(for: itemID)
|
||||
}
|
||||
|
||||
state.pillContextUpdater = { [weak self] pillContext in
|
||||
self?.pillContextUpdater(pillContext)
|
||||
}
|
||||
|
||||
state.timelineState.paginationState = timelineController.paginationState
|
||||
buildTimelineViews(timelineItems: timelineController.timelineItems)
|
||||
|
||||
@@ -826,6 +834,72 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
actionsSubject.send(.displayMessageForwarding(forwardingItem: .init(id: itemID, roomID: roomProxy.id, content: content)))
|
||||
}
|
||||
|
||||
// MARK: Pills
|
||||
|
||||
private func pillContextUpdater(_ pillContext: PillContext) {
|
||||
switch pillContext.data.type {
|
||||
case let .user(id):
|
||||
let isOwnMention = id == state.ownUserID
|
||||
if let profile = state.members[id] {
|
||||
var name = id
|
||||
if let displayName = profile.displayName {
|
||||
name = "@\(displayName)"
|
||||
}
|
||||
pillContext.viewState = .mention(isOwnMention: isOwnMention, displayText: name)
|
||||
} else {
|
||||
pillContext.viewState = .mention(isOwnMention: isOwnMention, displayText: id)
|
||||
pillContext.cancellable = context.$viewState
|
||||
.compactMap { $0.members[id] }
|
||||
.sink { [weak pillContext] profile in
|
||||
guard let pillContext else {
|
||||
return
|
||||
}
|
||||
|
||||
var name = id
|
||||
if let displayName = profile.displayName {
|
||||
name = "@\(displayName)"
|
||||
}
|
||||
pillContext.viewState = .mention(isOwnMention: isOwnMention, displayText: name)
|
||||
pillContext.cancellable = nil
|
||||
}
|
||||
}
|
||||
case .allUsers:
|
||||
pillContext.viewState = .mention(isOwnMention: true, displayText: PillConstants.atRoom)
|
||||
case .event(let room):
|
||||
var pillViewState: PillViewState = .reference(avatar: .link, displayText: L10n.screenRoomEventPill(room.value))
|
||||
defer {
|
||||
pillContext.viewState = pillViewState
|
||||
}
|
||||
|
||||
switch room {
|
||||
case .roomAlias(let alias):
|
||||
guard let roomSummary = clientProxy.roomSummaryForAlias(alias) else {
|
||||
return
|
||||
}
|
||||
// We always show the link image for event permalinks
|
||||
pillViewState = .reference(avatar: .link, displayText: L10n.screenRoomEventPill(roomSummary.name))
|
||||
case .roomID(let id):
|
||||
guard let roomSummary = clientProxy.roomSummaryForIdentifier(id) else {
|
||||
return
|
||||
}
|
||||
// We always show the link image for event permalinks
|
||||
pillViewState = .reference(avatar: .link, displayText: L10n.screenRoomEventPill(roomSummary.name))
|
||||
}
|
||||
case .roomAlias(let alias):
|
||||
guard let roomSummary = clientProxy.roomSummaryForAlias(alias) else {
|
||||
pillContext.viewState = .reference(avatar: .link, displayText: alias)
|
||||
return
|
||||
}
|
||||
pillContext.viewState = .reference(avatar: .roomAvatar(roomSummary.avatar), displayText: roomSummary.name)
|
||||
case .roomID(let id):
|
||||
guard let roomSummary = clientProxy.roomSummaryForIdentifier(id) else {
|
||||
pillContext.viewState = .reference(avatar: .link, displayText: id)
|
||||
return
|
||||
}
|
||||
pillContext.viewState = .reference(avatar: .roomAvatar(roomSummary.avatar), displayText: roomSummary.name)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User Indicators
|
||||
|
||||
private func showFocusLoadingIndicator() {
|
||||
@@ -896,7 +970,8 @@ extension TimelineViewModel {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,8 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
return mock
|
||||
}()
|
||||
|
||||
|
||||
@@ -339,7 +339,8 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
}()
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
@@ -88,8 +88,9 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
|
||||
static let singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")]
|
||||
static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"),
|
||||
ReadReceipt(userID: RoomMemberProxyMock.mockBob.userID, formattedTimestamp: "Before")]
|
||||
|
||||
@@ -142,8 +142,6 @@ struct FormattedBodyText_Previews: PreviewProvider, TestablePreview {
|
||||
let htmlStrings = [
|
||||
"""
|
||||
Plain text\n
|
||||
@bob:matrix.org\n
|
||||
#room:matrix.org\n
|
||||
!room:matrix.org\n
|
||||
https://www.matrix.org\n
|
||||
www.matrix.org\n
|
||||
|
||||
+2
-1
@@ -98,7 +98,8 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
|
||||
@@ -91,7 +91,8 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
clientProxy: ClientProxyMock(.init()))
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
|
||||
Reference in New Issue
Block a user