diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift index 19f936bdb..8c9b210ee 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift @@ -27,6 +27,8 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol { private var cancellables = Set() + private var updateMembersTask: Task? + 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 diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift index 135912a10..6cd3ca3f7 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift @@ -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) - } } } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreenUserStatusRow.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreenUserStatusRow.swift index 577fd866e..98328b419 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreenUserStatusRow.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreenUserStatusRow.swift @@ -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) diff --git a/UnitTests/Sources/CompletionSuggestionServiceTests.swift b/UnitTests/Sources/CompletionSuggestionServiceTests.swift index 96c7c35c2..92520f990 100644 --- a/UnitTests/Sources/CompletionSuggestionServiceTests.swift +++ b/UnitTests/Sources/CompletionSuggestionServiceTests.swift @@ -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.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