added the avatar and the room title as subtitle in the thread timeline as a principal action

also added a fix for the erroneous reply in thread action while in a thread, and also discovered that we can lead align the principal action again on iOS 26 (so it was a beta issue)
This commit is contained in:
Mauro Romito
2025-09-16 17:19:07 +02:00
committed by Mauro
parent a1038f0ffe
commit 826230001b
6 changed files with 79 additions and 14 deletions
@@ -54,7 +54,7 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol {
init(parameters: ThreadTimelineScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = ThreadTimelineScreenViewModel(roomProxy: parameters.roomProxy)
viewModel = ThreadTimelineScreenViewModel(roomProxy: parameters.roomProxy, userSession: parameters.userSession)
timelineViewModel = TimelineViewModel(roomProxy: parameters.roomProxy,
timelineController: parameters.timelineController,
@@ -10,7 +10,10 @@ import Foundation
enum ThreadTimelineScreenViewModelAction { }
struct ThreadTimelineScreenViewState: BindableState {
var roomTitle: String
var roomAvatar: RoomAvatar
var canSendMessage = true
var dmRecipientVerificationState: UserIdentityVerificationState?
var bindings = ThreadTimelineScreenViewStateBindings()
}
@@ -12,16 +12,20 @@ typealias ThreadTimelineScreenViewModelType = StateStoreViewModel<ThreadTimeline
class ThreadTimelineScreenViewModel: ThreadTimelineScreenViewModelType, ThreadTimelineScreenViewModelProtocol {
private let roomProxy: JoinedRoomProxyProtocol
private let userSession: UserSessionProtocol
private let actionsSubject: PassthroughSubject<ThreadTimelineScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<ThreadTimelineScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(roomProxy: JoinedRoomProxyProtocol) {
init(roomProxy: JoinedRoomProxyProtocol,
userSession: UserSessionProtocol) {
self.roomProxy = roomProxy
self.userSession = userSession
super.init(initialViewState: ThreadTimelineScreenViewState())
super.init(initialViewState: ThreadTimelineScreenViewState(roomTitle: roomProxy.infoPublisher.value.displayName ?? roomProxy.id,
roomAvatar: roomProxy.infoPublisher.value.avatar), mediaProvider: userSession.mediaProvider)
roomProxy.infoPublisher
.receive(on: DispatchQueue.main)
@@ -30,7 +34,20 @@ class ThreadTimelineScreenViewModel: ThreadTimelineScreenViewModelType, ThreadTi
}
.store(in: &cancellables)
let identityStatusChangesPublisher = roomProxy.identityStatusChangesPublisher.receive(on: DispatchQueue.main)
Task { [weak self] in
for await _ in identityStatusChangesPublisher.values {
guard !Task.isCancelled else {
return
}
await self?.updateVerificationBadge()
}
}
.store(in: &cancellables)
updateRoomInfo(roomProxy.infoPublisher.value)
Task { await updateVerificationBadge() }
}
// MARK: - Public
@@ -58,7 +75,26 @@ class ThreadTimelineScreenViewModel: ThreadTimelineScreenViewModelType, ThreadTi
// MARK: - Private
private func updateVerificationBadge() async {
guard roomProxy.isDirectOneToOneRoom,
let dmRecipient = roomProxy.membersPublisher.value.first(where: { $0.userID != roomProxy.ownUserID }),
case let .success(userIdentity) = await userSession.clientProxy.userIdentity(for: dmRecipient.userID) else {
state.dmRecipientVerificationState = .notVerified
return
}
guard let userIdentity else {
MXLog.failure("User identity should be known at this point")
state.dmRecipientVerificationState = .notVerified
return
}
state.dmRecipientVerificationState = userIdentity.verificationState
}
private func updateRoomInfo(_ roomInfo: RoomInfoProxyProtocol) {
state.roomTitle = roomInfo.displayName ?? roomProxy.id
state.roomAvatar = roomInfo.avatar
if let powerLevels = roomInfo.powerLevels {
state.canSendMessage = powerLevels.canOwnUser(sendMessage: .roomMessage)
}
@@ -23,8 +23,9 @@ struct ThreadTimelineScreen: View {
var body: some View {
TimelineView(timelineContext: timelineContext)
.navigationTitle("Thread")
.navigationTitle(L10n.commonThread)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
.background(.compound.bgCanvasDefault)
.toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background.
.timelineMediaPreview(viewModel: $context.mediaPreviewViewModel)
@@ -53,6 +54,24 @@ struct ThreadTimelineScreen: View {
}
}
@ToolbarContentBuilder
private var toolbar: some ToolbarContent {
// .principal + .primaryAction works better than .navigation leading + trailing
// as the latter disables interaction in the action button for rooms with long names
ToolbarItem(placement: .principal) {
RoomHeaderView(roomName: L10n.commonThread,
roomSubtitle: context.viewState.roomTitle,
roomAvatar: context.viewState.roomAvatar,
dmRecipientVerificationState: context.viewState.dmRecipientVerificationState,
mediaProvider: context.mediaProvider)
// Using a button stops it from getting truncated in the navigation bar
.contentShape(.rect)
}
if #available(iOS 26, *) {
ToolbarSpacer()
}
}
private var isAtBottomAndLive: Bool {
timelineContext.isScrolledToBottom && timelineContext.viewState.timelineState.isLive
}