mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 19:44:05 +00:00
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:
@@ -27,6 +27,8 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
|
|||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
private var updateMembersTask: Task<Void, Never>?
|
||||||
|
|
||||||
init(roomProxy: JoinedRoomProxyProtocol,
|
init(roomProxy: JoinedRoomProxyProtocol,
|
||||||
roomListPublisher: AnyPublisher<[RoomSummary], Never>) {
|
roomListPublisher: AnyPublisher<[RoomSummary], Never>) {
|
||||||
self.roomProxy = roomProxy
|
self.roomProxy = roomProxy
|
||||||
@@ -66,7 +68,17 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setSuggestionTrigger(_ suggestionTrigger: SuggestionTrigger?) {
|
func setSuggestionTrigger(_ suggestionTrigger: SuggestionTrigger?) {
|
||||||
|
let isNewUserMention = suggestionTrigger?.type == .user && suggestionTriggerSubject.value?.type != .user
|
||||||
|
|
||||||
suggestionTriggerSubject.value = suggestionTrigger
|
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
|
// MARK: - Private
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ struct SettingsScreen: View {
|
|||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbarVisibility(context.viewState.navigationBarVisibility, for: .navigationBar)
|
.toolbarVisibility(context.viewState.navigationBarVisibility, for: .navigationBar)
|
||||||
.toolbar { toolbar }
|
.toolbar { toolbar }
|
||||||
|
.sheet(isPresented: $context.isPresentingStatusPicker) {
|
||||||
|
SettingsScreenUserStatusPickerView { action in
|
||||||
|
context.send(viewAction: .userStatus(action))
|
||||||
|
}
|
||||||
|
.presentationDetents([.medium])
|
||||||
|
.presentationBackground(.compound.bgCanvasDefault)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var userSection: some View {
|
private var userSection: some View {
|
||||||
@@ -94,13 +101,6 @@ struct SettingsScreen: View {
|
|||||||
SettingsScreenUserStatusRow(mode: context.viewState.userStatusRowMode) { action in
|
SettingsScreenUserStatusRow(mode: context.viewState.userStatusRowMode) { action in
|
||||||
context.send(viewAction: .userStatus(action))
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-1
@@ -9,13 +9,33 @@ import Compound
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SettingsScreenUserStatusRow: View {
|
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 mode: Mode
|
||||||
let action: (SettingsScreenViewAction.UserStatusAction) -> Void
|
let action: (SettingsScreenViewAction.UserStatusAction) -> Void
|
||||||
|
|
||||||
@State private var customText = ""
|
@State private var customText = ""
|
||||||
|
@FocusState private var isCustomFieldFocused: Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
rowContent
|
||||||
|
.onChange(of: mode.isCustomStatusInput) { _, newValue in
|
||||||
|
guard newValue else { return }
|
||||||
|
customText = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var rowContent: some View {
|
||||||
switch mode {
|
switch mode {
|
||||||
case .pickStatusButton:
|
case .pickStatusButton:
|
||||||
ListRow(label: .default(title: L10n.screenSettingsUserStatusPlaceholder, icon: \.reaction),
|
ListRow(label: .default(title: L10n.screenSettingsUserStatusPlaceholder, icon: \.reaction),
|
||||||
@@ -34,6 +54,8 @@ struct SettingsScreenUserStatusRow: View {
|
|||||||
|
|
||||||
TextField(L10n.screenSettingsUserStatusCustomHint, text: $customText)
|
TextField(L10n.screenSettingsUserStatusCustomHint, text: $customText)
|
||||||
.textFieldStyle(.compound(.raised))
|
.textFieldStyle(.compound(.raised))
|
||||||
|
.focused($isCustomFieldFocused)
|
||||||
|
.onAppear { isCustomFieldFocused = true }
|
||||||
.accessibilityLabel(L10n.screenSettingsUserStatusCustomHint)
|
.accessibilityLabel(L10n.screenSettingsUserStatusCustomHint)
|
||||||
.padding(.vertical, 3)
|
.padding(.vertical, 3)
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
// Please see LICENSE files in the repository root for full details.
|
// Please see LICENSE files in the repository root for full details.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Combine
|
|
||||||
@testable import ElementX
|
@testable import ElementX
|
||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
struct CompletionSuggestionServiceTests {
|
struct CompletionSuggestionServiceTests {
|
||||||
|
// MARK: User Suggestions
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func userSuggestions() async throws {
|
func userSuggestions() async throws {
|
||||||
let alice: RoomMemberProxyMock = .mockAlice
|
let alice: RoomMemberProxyMock = .mockAlice
|
||||||
@@ -178,6 +179,40 @@ struct CompletionSuggestionServiceTests {
|
|||||||
try await deffered.fulfill()
|
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
|
@Test
|
||||||
func roomSuggestions() async throws {
|
func roomSuggestions() async throws {
|
||||||
let alice: RoomMemberProxyMock = .mockAlice
|
let alice: RoomMemberProxyMock = .mockAlice
|
||||||
@@ -353,6 +388,8 @@ struct CompletionSuggestionServiceTests {
|
|||||||
try await deffered.fulfill()
|
try await deffered.fulfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Common
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func suggestionsWithMultipleDifferentTriggers() async throws {
|
func suggestionsWithMultipleDifferentTriggers() async throws {
|
||||||
let alice: RoomMemberProxyMock = .mockAlice
|
let alice: RoomMemberProxyMock = .mockAlice
|
||||||
|
|||||||
Reference in New Issue
Block a user