More tweaks for User Status. (#6051)

* Make sure user profiles are up to date when mentioning.

* Automatically focus the custom status text field.

* Make sure to clear the previous custom status text before input.

* Attempt to fix the user status sheet sometimes disappearing.
This commit is contained in:
Doug
2026-08-21 11:23:33 +01:00
committed by GitHub
parent dd84da15b0
commit 0d222e8ac0
4 changed files with 80 additions and 9 deletions
@@ -27,6 +27,8 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
private var cancellables = Set<AnyCancellable>()
private var updateMembersTask: Task<Void, Never>?
init(roomProxy: JoinedRoomProxyProtocol,
roomListPublisher: AnyPublisher<[RoomSummary], Never>) {
self.roomProxy = roomProxy
@@ -66,7 +68,17 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
}
func setSuggestionTrigger(_ suggestionTrigger: SuggestionTrigger?) {
let isNewUserMention = suggestionTrigger?.type == .user && suggestionTriggerSubject.value?.type != .user
suggestionTriggerSubject.value = suggestionTrigger
// Refresh the members each time a new user suggestion is triggered to pull in the latest profiles.
guard isNewUserMention, updateMembersTask == nil else { return }
updateMembersTask = Task { [weak self, roomProxy] in
await roomProxy.updateMembers()
self?.updateMembersTask = nil
}
}
// MARK: - Private
@@ -46,6 +46,13 @@ struct SettingsScreen: View {
.navigationBarTitleDisplayMode(.inline)
.toolbarVisibility(context.viewState.navigationBarVisibility, for: .navigationBar)
.toolbar { toolbar }
.sheet(isPresented: $context.isPresentingStatusPicker) {
SettingsScreenUserStatusPickerView { action in
context.send(viewAction: .userStatus(action))
}
.presentationDetents([.medium])
.presentationBackground(.compound.bgCanvasDefault)
}
}
private var userSection: some View {
@@ -94,13 +101,6 @@ struct SettingsScreen: View {
SettingsScreenUserStatusRow(mode: context.viewState.userStatusRowMode) { action in
context.send(viewAction: .userStatus(action))
}
.sheet(isPresented: $context.isPresentingStatusPicker) {
SettingsScreenUserStatusPickerView { action in
context.send(viewAction: .userStatus(action))
}
.presentationDetents([.medium]) // Stop using a List and calculate the exact height?
.presentationBackground(.compound.bgCanvasDefault)
}
}
}
@@ -9,13 +9,33 @@ import Compound
import SwiftUI
struct SettingsScreenUserStatusRow: View {
enum Mode: Equatable { case pickStatusButton, customStatusInput(emoji: Character), showingStatus(UserStatus.Displayed) }
enum Mode: Equatable {
case pickStatusButton, customStatusInput(emoji: Character), showingStatus(UserStatus.Displayed)
var isCustomStatusInput: Bool {
switch self {
case .customStatusInput: true
default: false
}
}
}
let mode: Mode
let action: (SettingsScreenViewAction.UserStatusAction) -> Void
@State private var customText = ""
@FocusState private var isCustomFieldFocused: Bool
var body: some View {
rowContent
.onChange(of: mode.isCustomStatusInput) { _, newValue in
guard newValue else { return }
customText = ""
}
}
@ViewBuilder
var rowContent: some View {
switch mode {
case .pickStatusButton:
ListRow(label: .default(title: L10n.screenSettingsUserStatusPlaceholder, icon: \.reaction),
@@ -34,6 +54,8 @@ struct SettingsScreenUserStatusRow: View {
TextField(L10n.screenSettingsUserStatusCustomHint, text: $customText)
.textFieldStyle(.compound(.raised))
.focused($isCustomFieldFocused)
.onAppear { isCustomFieldFocused = true }
.accessibilityLabel(L10n.screenSettingsUserStatusCustomHint)
.padding(.vertical, 3)
@@ -6,12 +6,13 @@
// Please see LICENSE files in the repository root for full details.
//
import Combine
@testable import ElementX
import Testing
@MainActor
struct CompletionSuggestionServiceTests {
// MARK: User Suggestions
@Test
func userSuggestions() async throws {
let alice: RoomMemberProxyMock = .mockAlice
@@ -178,6 +179,40 @@ struct CompletionSuggestionServiceTests {
try await deffered.fulfill()
}
@Test
func userSuggestionRefreshesMembers() async throws {
let members: [RoomMemberProxyMock] = [.mockAlice, .mockBob, .mockMe]
let roomProxyMock = JoinedRoomProxyMock(.init(id: "roomID", name: "test", members: members))
let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
let (refreshesStream, refreshesContinuation) = AsyncStream<Void>.makeStream()
roomProxyMock.updateMembersClosure = { refreshesContinuation.yield() }
let service = CompletionSuggestionService(roomProxy: roomProxyMock,
roomListPublisher: roomSummaryProvider.roomListPublisher.eraseToAnyPublisher())
#expect(!roomProxyMock.updateMembersCalled)
// Beginning a mention refreshes the members once…
var deferred = deferFulfillment(refreshesStream) { _ in true }
service.setSuggestionTrigger(.init(type: .user, text: "", range: .init()))
try await deferred.fulfill()
#expect(roomProxyMock.updateMembersCallsCount == 1)
// …but typing further within the same mention doesn't refresh again.
service.setSuggestionTrigger(.init(type: .user, text: "a", range: .init()))
service.setSuggestionTrigger(.init(type: .user, text: "al", range: .init()))
#expect(roomProxyMock.updateMembersCallsCount == 1)
// Clearing and starting a new mention refreshes a second time.
deferred = deferFulfillment(refreshesStream) { _ in true }
service.setSuggestionTrigger(nil)
service.setSuggestionTrigger(.init(type: .user, text: "", range: .init()))
try await deferred.fulfill()
#expect(roomProxyMock.updateMembersCallsCount == 2)
}
// MARK: Room Suggestions
@Test
func roomSuggestions() async throws {
let alice: RoomMemberProxyMock = .mockAlice
@@ -353,6 +388,8 @@ struct CompletionSuggestionServiceTests {
try await deffered.fulfill()
}
// MARK: Common
@Test
func suggestionsWithMultipleDifferentTriggers() async throws {
let alice: RoomMemberProxyMock = .mockAlice