mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Create & Join rooms UI/UX tweaks (#1005)
* Fix people icon size * Disable dismiss when loading in StartChatScreen * Reduce debounce on search * Add padding to invite button * Add show/hide bottom bar behaviour * Fix bg color in StartChatScreen form * Update localisations * Update search users results to 10 * Fix form background in InviteUsersScreen * Fix invites list bg color * Add mxid in InvitesScreenCell * Refactor InvitesScreenCell design * Fix separator spacing * Add more design fixes * Refine InvitesScreenCell layout * Refactor UserProfileCell design * Refactor FormRowAccessory * Update localisations * Fix camera size and security section padding * Change create button style * Refine InviteUsersScreenSelectedItem * Refine CreateRoomScreen * Refine UserProfileCell padding * Amend preview name * delete test code * Add separator in security section * Fix spacing in InviteUsersScreenSelectedItem * Revert confirmationAction in CreateRoomScreen * Refine HomeScreenInvitesButton * Add dismiss keyboard in InviteUsersScreen * Refactor FormRowAccessory * Remove isDisabled from FormButtonStyles * Update localisations * Cleanup RoomDetailsEditScreen * UI fixes on horizontal user carousel * Refactor InviteUsersScreenSelectedItem * Try fix flaky tests * Fix typo * Update snapshot tests * Fix flaky UTs * Fix HomeScreen ui tests * Fix scroll on selection * Refactor RoomFlowCoordinatorTests
This commit is contained in:
@@ -198,6 +198,8 @@ public enum L10n {
|
||||
public static var commonReportSubmitted: String { return L10n.tr("Localizable", "common_report_submitted") }
|
||||
/// Room name
|
||||
public static var commonRoomName: String { return L10n.tr("Localizable", "common_room_name") }
|
||||
/// e.g. your project name
|
||||
public static var commonRoomNamePlaceholder: String { return L10n.tr("Localizable", "common_room_name_placeholder") }
|
||||
/// Search for someone
|
||||
public static var commonSearchForSomeone: String { return L10n.tr("Localizable", "common_search_for_someone") }
|
||||
/// Search results
|
||||
@@ -224,6 +226,8 @@ public enum L10n {
|
||||
public static var commonSuggestions: String { return L10n.tr("Localizable", "common_suggestions") }
|
||||
/// Topic
|
||||
public static var commonTopic: String { return L10n.tr("Localizable", "common_topic") }
|
||||
/// What is this room about?
|
||||
public static var commonTopicPlaceholder: String { return L10n.tr("Localizable", "common_topic_placeholder") }
|
||||
/// Unable to decrypt
|
||||
public static var commonUnableToDecrypt: String { return L10n.tr("Localizable", "common_unable_to_decrypt") }
|
||||
/// We were unable to successfully send invites to one or more users.
|
||||
@@ -492,9 +496,9 @@ public enum L10n {
|
||||
public static var screenChangeServerTitle: String { return L10n.tr("Localizable", "screen_change_server_title") }
|
||||
/// New room
|
||||
public static var screenCreateRoomActionCreateRoom: String { return L10n.tr("Localizable", "screen_create_room_action_create_room") }
|
||||
/// Invite people
|
||||
/// Invite friends to Element
|
||||
public static var screenCreateRoomActionInvitePeople: String { return L10n.tr("Localizable", "screen_create_room_action_invite_people") }
|
||||
/// Add people
|
||||
/// Invite people
|
||||
public static var screenCreateRoomAddPeopleTitle: String { return L10n.tr("Localizable", "screen_create_room_add_people_title") }
|
||||
/// An error occurred when creating the room
|
||||
public static var screenCreateRoomErrorCreatingRoom: String { return L10n.tr("Localizable", "screen_create_room_error_creating_room") }
|
||||
@@ -508,14 +512,10 @@ public enum L10n {
|
||||
public static var screenCreateRoomPublicOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_public_option_title") }
|
||||
/// Room name
|
||||
public static var screenCreateRoomRoomNameLabel: String { return L10n.tr("Localizable", "screen_create_room_room_name_label") }
|
||||
/// e.g. Product Sprint
|
||||
public static var screenCreateRoomRoomNamePlaceholder: String { return L10n.tr("Localizable", "screen_create_room_room_name_placeholder") }
|
||||
/// Create a room
|
||||
public static var screenCreateRoomTitle: String { return L10n.tr("Localizable", "screen_create_room_title") }
|
||||
/// Topic (optional)
|
||||
public static var screenCreateRoomTopicLabel: String { return L10n.tr("Localizable", "screen_create_room_topic_label") }
|
||||
/// What is this room about?
|
||||
public static var screenCreateRoomTopicPlaceholder: String { return L10n.tr("Localizable", "screen_create_room_topic_placeholder") }
|
||||
/// Block
|
||||
public static var screenDmDetailsBlockAlertAction: String { return L10n.tr("Localizable", "screen_dm_details_block_alert_action") }
|
||||
/// Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.
|
||||
@@ -542,9 +542,9 @@ public enum L10n {
|
||||
public static var screenInvitesDeclineDirectChatTitle: String { return L10n.tr("Localizable", "screen_invites_decline_direct_chat_title") }
|
||||
/// No Invites
|
||||
public static var screenInvitesEmptyList: String { return L10n.tr("Localizable", "screen_invites_empty_list") }
|
||||
/// %1$@ invited you
|
||||
public static func screenInvitesInvitedYou(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_invites_invited_you", String(describing: p1))
|
||||
/// %1$@ (%2$@) invited you
|
||||
public static func screenInvitesInvitedYou(_ p1: Any, _ p2: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_invites_invited_you", String(describing: p1), String(describing: p2))
|
||||
}
|
||||
/// This account has been deactivated.
|
||||
public static var screenLoginErrorDeactivatedAccount: String { return L10n.tr("Localizable", "screen_login_error_deactivated_account") }
|
||||
@@ -576,6 +576,14 @@ public enum L10n {
|
||||
public static var screenMediaUploadPreviewErrorFailedProcessing: String { return L10n.tr("Localizable", "screen_media_upload_preview_error_failed_processing") }
|
||||
/// Failed uploading media, please try again.
|
||||
public static var screenMediaUploadPreviewErrorFailedSending: String { return L10n.tr("Localizable", "screen_media_upload_preview_error_failed_sending") }
|
||||
/// Sign in manually
|
||||
public static var screenOnboardingSignInManually: String { return L10n.tr("Localizable", "screen_onboarding_sign_in_manually") }
|
||||
/// Sign in with QR code
|
||||
public static var screenOnboardingSignInWithQrCode: String { return L10n.tr("Localizable", "screen_onboarding_sign_in_with_qr_code") }
|
||||
/// Create account
|
||||
public static var screenOnboardingSignUp: String { return L10n.tr("Localizable", "screen_onboarding_sign_up") }
|
||||
/// Communicate and collaborate securely
|
||||
public static var screenOnboardingSubtitle: String { return L10n.tr("Localizable", "screen_onboarding_subtitle") }
|
||||
/// Welcome to the %1$@ Beta. Supercharged, for speed and simplicity.
|
||||
public static func screenOnboardingWelcomeSubtitle(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_onboarding_welcome_subtitle", String(describing: p1))
|
||||
@@ -618,6 +626,8 @@ public enum L10n {
|
||||
public static var screenRoomDetailsLeaveRoomTitle: String { return L10n.tr("Localizable", "screen_room_details_leave_room_title") }
|
||||
/// People
|
||||
public static var screenRoomDetailsPeopleTitle: String { return L10n.tr("Localizable", "screen_room_details_people_title") }
|
||||
/// Room name
|
||||
public static var screenRoomDetailsRoomNameLabel: String { return L10n.tr("Localizable", "screen_room_details_room_name_label") }
|
||||
/// Security
|
||||
public static var screenRoomDetailsSecurityTitle: String { return L10n.tr("Localizable", "screen_room_details_security_title") }
|
||||
/// Share room
|
||||
|
||||
@@ -33,4 +33,8 @@ extension UserProfileProxy {
|
||||
static var mockCharlie: UserProfileProxy {
|
||||
.init(userID: "@charlie:matrix.org", displayName: "Charlie", avatarURL: nil)
|
||||
}
|
||||
|
||||
static var mockVerbose: UserProfileProxy {
|
||||
.init(userID: "@charlie:matrix.org", displayName: "Charlie is the best display name", avatarURL: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ extension Publisher where Output == String, Failure == Never {
|
||||
/// Clearing the text publishes the update immediately.
|
||||
func debounceAndRemoveDuplicates() -> AnyPublisher<String, Never> {
|
||||
map { query in
|
||||
let milliseconds = query.isEmpty ? 0 : 500
|
||||
let milliseconds = query.isEmpty ? 0 : 250
|
||||
return Just(query).delay(for: .milliseconds(milliseconds), scheduler: DispatchQueue.main)
|
||||
}
|
||||
.switchToLatest()
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Copyright 2023 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension Section where Parent == Color, Content == EmptyView, Footer == EmptyView {
|
||||
// An empty section whose purpose is to keep Form's background color when there is no content into it.
|
||||
static var empty: some View {
|
||||
Section {
|
||||
EmptyView()
|
||||
} header: {
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,11 @@ import Combine
|
||||
import UIKit
|
||||
|
||||
class ScrollViewAdapter: NSObject, UIScrollViewDelegate {
|
||||
enum ScrollDirection {
|
||||
case up
|
||||
case down
|
||||
}
|
||||
|
||||
var scrollView: UIScrollView? {
|
||||
didSet {
|
||||
oldValue?.delegate = nil
|
||||
@@ -25,13 +30,28 @@ class ScrollViewAdapter: NSObject, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
let didScroll = PassthroughSubject<Void, Never>()
|
||||
let isScrolling = CurrentValueSubject<Bool, Never>(false)
|
||||
private let didScrollSubject = PassthroughSubject<Void, Never>()
|
||||
var didScroll: AnyPublisher<Void, Never> {
|
||||
didScrollSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private let isScrollingSubject = CurrentValueSubject<Bool, Never>(false)
|
||||
var isScrolling: CurrentValuePublisher<Bool, Never> {
|
||||
.init(isScrollingSubject)
|
||||
}
|
||||
|
||||
private let scrollDirectionSubject: PassthroughSubject<ScrollDirection, Never> = .init()
|
||||
var scrollDirection: AnyPublisher<ScrollDirection, Never> {
|
||||
scrollDirectionSubject
|
||||
.removeDuplicates()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// MARK: - UIScrollViewDelegate
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
didScroll.send(())
|
||||
didScrollSubject.send(())
|
||||
updateScrollDirection(scrollView)
|
||||
}
|
||||
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
@@ -57,6 +77,20 @@ class ScrollViewAdapter: NSObject, UIScrollViewDelegate {
|
||||
// MARK: - Private
|
||||
|
||||
private func updateDidScroll(_ scrollView: UIScrollView) {
|
||||
isScrolling.send(scrollView.isDragging || scrollView.isDecelerating)
|
||||
isScrollingSubject.send(scrollView.isDragging || scrollView.isDecelerating)
|
||||
}
|
||||
|
||||
private func updateScrollDirection(_ scrollView: UIScrollView) {
|
||||
let velocity = scrollView.panGestureRecognizer.velocity(in: nil)
|
||||
|
||||
if velocity.y > Constant.scrollDirectionThreshold {
|
||||
scrollDirectionSubject.send(.down)
|
||||
} else if velocity.y < -Constant.scrollDirectionThreshold {
|
||||
scrollDirectionSubject.send(.up)
|
||||
}
|
||||
}
|
||||
|
||||
private enum Constant {
|
||||
static let scrollDirectionThreshold: CGFloat = 200
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,31 @@
|
||||
import SwiftUI
|
||||
|
||||
/// A view to be added on the trailing edge of a form row.
|
||||
enum FormRowAccessory: View {
|
||||
case navigationLink
|
||||
case progressView
|
||||
case selection(isSelected: Bool)
|
||||
struct FormRowAccessory: View {
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
|
||||
enum Kind {
|
||||
case navigationLink
|
||||
case progressView
|
||||
case selection(isSelected: Bool)
|
||||
}
|
||||
|
||||
let kind: Kind
|
||||
|
||||
static var navigationLink: Self {
|
||||
.init(kind: .navigationLink)
|
||||
}
|
||||
|
||||
static var progressView: Self {
|
||||
.init(kind: .progressView)
|
||||
}
|
||||
|
||||
static func selection(isSelected: Bool) -> Self {
|
||||
.init(kind: .selection(isSelected: isSelected))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
switch self {
|
||||
switch kind {
|
||||
case .navigationLink:
|
||||
Image(systemName: "chevron.forward")
|
||||
.font(.compound.bodyMD.bold())
|
||||
@@ -33,9 +51,13 @@ enum FormRowAccessory: View {
|
||||
case .selection(let isSelected):
|
||||
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundColor(isSelected ? .element.primaryContent : .element.tertiaryContent)
|
||||
.foregroundColor(isSelected && isEnabled ? .element.primaryContent : .element.tertiaryContent)
|
||||
}
|
||||
}
|
||||
|
||||
private init(kind: Kind) {
|
||||
self.kind = kind
|
||||
}
|
||||
}
|
||||
|
||||
/// Default button styling for form rows.
|
||||
@@ -44,8 +66,6 @@ enum FormRowAccessory: View {
|
||||
/// to change the background colour depending on whether the button is currently pressed or not.
|
||||
struct FormButtonStyle: PrimitiveButtonStyle {
|
||||
var iconAlignment: VerticalAlignment = .firstTextBaseline
|
||||
/// Disables the button while providing an entry point for UI customizations
|
||||
var isDisabled = false
|
||||
/// An accessory to be added on the trailing side of the row.
|
||||
var accessory: FormRowAccessory?
|
||||
|
||||
@@ -55,14 +75,12 @@ struct FormButtonStyle: PrimitiveButtonStyle {
|
||||
.labelStyle(FormRowLabelStyle(alignment: iconAlignment, role: configuration.role))
|
||||
.frame(maxHeight: .infinity) // Make sure the label fills the cell vertically.
|
||||
}
|
||||
.buttonStyle(Style(isDisabled: isDisabled, accessory: accessory))
|
||||
.buttonStyle(Style(accessory: accessory))
|
||||
.listRowInsets(EdgeInsets()) // Remove insets so the background fills the cell.
|
||||
.disabled(isDisabled)
|
||||
}
|
||||
|
||||
/// Inner style used to set the pressed background colour.
|
||||
struct Style: ButtonStyle {
|
||||
var isDisabled: Bool
|
||||
private struct Style: ButtonStyle {
|
||||
var accessory: FormRowAccessory?
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
@@ -73,7 +91,6 @@ struct FormButtonStyle: PrimitiveButtonStyle {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
accessory
|
||||
.opacity(isDisabled ? 0.54 : 1)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.padding(FormRow.insets) // Re-apply the normal insets using padding.
|
||||
@@ -135,14 +152,18 @@ struct FormButtonStyles_Previews: PreviewProvider {
|
||||
Text("Hello world")
|
||||
}
|
||||
.buttonStyle(FormButtonStyle())
|
||||
|
||||
Button { } label: {
|
||||
Text("Selected")
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: true)))
|
||||
|
||||
Button { } label: {
|
||||
Text("Selected (disabled)")
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(isDisabled: true, accessory: .selection(isSelected: true)))
|
||||
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: true)))
|
||||
.disabled(true)
|
||||
|
||||
Button { } label: {
|
||||
Text("Unselected")
|
||||
}
|
||||
|
||||
@@ -29,10 +29,9 @@ struct UserProfileCell: View {
|
||||
contentID: user.userID,
|
||||
avatarSize: .user(on: .startChat),
|
||||
imageProvider: imageProvider)
|
||||
.padding(.vertical, 10)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(user.displayName ?? user.userID)
|
||||
.font(.compound.headingSM)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
@@ -40,6 +39,7 @@ struct UserProfileCell: View {
|
||||
subtitleContent
|
||||
|
||||
warningContent
|
||||
.padding(.top, 2)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
@@ -63,15 +63,14 @@ struct UserProfileCell: View {
|
||||
@ViewBuilder
|
||||
private var warningContent: some View {
|
||||
if !user.isVerified, membership == nil {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 5) {
|
||||
Text(Image(systemName: "exclamationmark.circle.fill"))
|
||||
.foregroundColor(.compound.textCriticalPrimary)
|
||||
|
||||
Text(L10n.commonInviteUnknownProfile)
|
||||
.foregroundColor(.secondary)
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
}
|
||||
.font(.compound.bodyXS)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,13 +108,15 @@ struct UserProfileCell_Previews: PreviewProvider {
|
||||
Button(action: action) {
|
||||
UserProfileCell(user: .mockCharlie, membership: .join, imageProvider: MockMediaProvider())
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(isDisabled: true, accessory: .selection(isSelected: true)))
|
||||
.disabled(true)
|
||||
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: true)))
|
||||
.previewDisplayName("Selected disabled user")
|
||||
|
||||
Button(action: action) {
|
||||
UserProfileCell(user: .init(userID: "@someone:matrix.org"), membership: .join, imageProvider: MockMediaProvider())
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(isDisabled: true, accessory: .selection(isSelected: true)))
|
||||
.disabled(true)
|
||||
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: true)))
|
||||
.previewDisplayName("Unverified joined user")
|
||||
|
||||
Button(action: action) {
|
||||
|
||||
@@ -26,32 +26,25 @@ struct CreateRoomScreen: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
mainContent
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.elementFormStyle()
|
||||
.navigationTitle(L10n.screenCreateRoomTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
createButton
|
||||
}
|
||||
}
|
||||
.background(ViewFrameReader(frame: $frame))
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
}
|
||||
|
||||
/// The main content of the view to be shown in a scroll view.
|
||||
var mainContent: some View {
|
||||
Form {
|
||||
roomSection
|
||||
topicSection
|
||||
Spacer()
|
||||
.listRowBackground(Color.clear)
|
||||
securitySection
|
||||
}
|
||||
.elementFormStyle()
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.navigationTitle(L10n.screenCreateRoomTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
createButton
|
||||
}
|
||||
}
|
||||
.background(ViewFrameReader(frame: $frame))
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
}
|
||||
|
||||
@ScaledMetric private var roomIconSize: CGFloat = 64
|
||||
@ScaledMetric private var roomIconSize: CGFloat = 70
|
||||
private var roomSection: some View {
|
||||
Section {
|
||||
HStack(alignment: .center, spacing: 16) {
|
||||
@@ -94,7 +87,7 @@ struct CreateRoomScreen: View {
|
||||
.formSectionHeader()
|
||||
TextField(L10n.screenCreateRoomRoomNameLabel,
|
||||
text: $context.roomName,
|
||||
prompt: Text(L10n.screenCreateRoomRoomNamePlaceholder),
|
||||
prompt: Text(L10n.commonRoomNamePlaceholder),
|
||||
axis: .horizontal)
|
||||
.focused($focus, equals: .name)
|
||||
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.roomName)
|
||||
@@ -111,6 +104,7 @@ struct CreateRoomScreen: View {
|
||||
|
||||
private var cameraImage: some View {
|
||||
Image(systemName: "camera")
|
||||
.font(.system(size: 28, weight: .semibold))
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.frame(width: roomIconSize, height: roomIconSize)
|
||||
.background(Color.element.quinaryContent)
|
||||
@@ -121,7 +115,7 @@ struct CreateRoomScreen: View {
|
||||
Section {
|
||||
TextField(L10n.screenCreateRoomTopicLabel,
|
||||
text: $context.roomTopic,
|
||||
prompt: Text(L10n.screenCreateRoomTopicPlaceholder),
|
||||
prompt: Text(L10n.commonTopicPlaceholder),
|
||||
axis: .vertical)
|
||||
.focused($focus, equals: .topic)
|
||||
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.roomTopic)
|
||||
@@ -138,11 +132,11 @@ struct CreateRoomScreen: View {
|
||||
}
|
||||
|
||||
@State private var frame: CGRect = .zero
|
||||
@ScaledMetric private var invitedUserCellWidth: CGFloat = 64
|
||||
@ScaledMetric private var invitedUserCellWidth: CGFloat = 72
|
||||
|
||||
private var selectedUsersSection: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(spacing: 28) {
|
||||
LazyHStack(spacing: 16) {
|
||||
ForEach(context.viewState.selectedUsers, id: \.userID) { user in
|
||||
InviteUsersScreenSelectedItem(user: user, imageProvider: context.imageProvider) {
|
||||
context.send(viewAction: .deselectUser(user))
|
||||
@@ -150,7 +144,7 @@ struct CreateRoomScreen: View {
|
||||
.frame(width: invitedUserCellWidth)
|
||||
}
|
||||
}
|
||||
.padding(.init(top: 16, leading: 32, bottom: 16, trailing: 32))
|
||||
.padding(.init(top: 22, leading: 20, bottom: 0, trailing: 32))
|
||||
}
|
||||
.frame(width: frame.width)
|
||||
}
|
||||
@@ -159,23 +153,26 @@ struct CreateRoomScreen: View {
|
||||
Section {
|
||||
Picker(L10n.commonSecurity, selection: $context.isRoomPrivate) {
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.screenCreateRoomPrivateOptionTitle)
|
||||
.font(.compound.bodyLG)
|
||||
Text(L10n.screenCreateRoomPrivateOptionDescription)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.font(.compound.bodyXS)
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "lock.shield")
|
||||
}
|
||||
.tag(true)
|
||||
.labelStyle(FormRowLabelStyle(alignment: .top))
|
||||
|
||||
Label {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(L10n.screenCreateRoomPublicOptionTitle)
|
||||
.font(.compound.bodyLG)
|
||||
Text(L10n.screenCreateRoomPublicOptionDescription)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.font(.compound.bodyXS)
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "exclamationmark.shield")
|
||||
@@ -188,8 +185,10 @@ struct CreateRoomScreen: View {
|
||||
} header: {
|
||||
Text(L10n.commonSecurity.uppercased())
|
||||
.formSectionHeader()
|
||||
.padding(.top, 40)
|
||||
}
|
||||
.formSectionStyle()
|
||||
.listRowSeparatorTint(.element.quinaryContent)
|
||||
.listRowBackground(Color.element.formRowBackground)
|
||||
}
|
||||
|
||||
private var createButton: some View {
|
||||
|
||||
@@ -26,6 +26,7 @@ struct HomeScreen: View {
|
||||
|
||||
@State private var scrollViewAdapter = ScrollViewAdapter()
|
||||
@State private var showingLogoutConfirmation = false
|
||||
@State private var showingBottomToolbar = true
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
@@ -99,6 +100,11 @@ struct HomeScreen: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(scrollViewAdapter.scrollDirection) { direction in
|
||||
withAnimation(.elementDefault) {
|
||||
showingBottomToolbar = (direction == .down)
|
||||
}
|
||||
}
|
||||
.onChange(of: context.viewState.visibleRooms) { _ in
|
||||
// Give the view a chance to update
|
||||
DispatchQueue.main.async {
|
||||
@@ -115,20 +121,27 @@ struct HomeScreen: View {
|
||||
actions: leaveRoomAlertActions,
|
||||
message: leaveRoomAlertMessage)
|
||||
.navigationTitle(L10n.screenRoomlistMainSpaceTitle)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
userMenuButton
|
||||
}
|
||||
if context.viewState.startChatFlowEnabled {
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
Spacer()
|
||||
newRoomButton
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar(showingBottomToolbar ? .automatic : .hidden, for: .bottomBar)
|
||||
.toolbar { toolbar }
|
||||
.background(Color.element.background.ignoresSafeArea())
|
||||
.track(screen: .home)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
userMenuButton
|
||||
}
|
||||
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
if context.viewState.startChatFlowEnabled {
|
||||
Spacer()
|
||||
newRoomButton
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var userMenuButton: some View {
|
||||
|
||||
@@ -34,6 +34,7 @@ struct HomeScreenInvitesButton: View {
|
||||
badge
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +47,7 @@ struct HomeScreenInvitesButton: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct InvitesButton_Previews: PreviewProvider {
|
||||
struct HomeScreenInvitesButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HomeScreenInvitesButton(title: "Invites", hasBadge: true, action: { })
|
||||
.previewDisplayName("Badge on")
|
||||
|
||||
@@ -55,6 +55,8 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
|
||||
actionsSubject.send(.invite(users: state.selectedUsers.map(\.userID)))
|
||||
}
|
||||
case .toggleUser(let user):
|
||||
let willSelectUser = !state.selectedUsers.contains(user)
|
||||
state.scrollToLastID = willSelectUser ? user.userID : nil
|
||||
actionsSubject.send(.toggleUser(user))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ struct InviteUsersScreen: View {
|
||||
var body: some View {
|
||||
mainContent
|
||||
.elementFormStyle()
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.navigationTitle(L10n.screenCreateRoomActionInvitePeople)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
@@ -69,33 +70,38 @@ struct InviteUsersScreen: View {
|
||||
.accessibilityIdentifier(A11yIdentifiers.startChatScreen.searchNoResults)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var usersSection: some View {
|
||||
Section {
|
||||
ForEach(context.viewState.usersSection.users, id: \.userID) { user in
|
||||
Button { context.send(viewAction: .toggleUser(user)) } label: {
|
||||
UserProfileCell(user: user,
|
||||
membership: context.viewState.membershipState(user),
|
||||
imageProvider: context.imageProvider)
|
||||
if !context.viewState.usersSection.users.isEmpty {
|
||||
Section {
|
||||
ForEach(context.viewState.usersSection.users, id: \.userID) { user in
|
||||
Button { context.send(viewAction: .toggleUser(user)) } label: {
|
||||
UserProfileCell(user: user,
|
||||
membership: context.viewState.membershipState(user),
|
||||
imageProvider: context.imageProvider)
|
||||
}
|
||||
.disabled(context.viewState.isUserDisabled(user))
|
||||
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: context.viewState.isUserSelected(user))))
|
||||
}
|
||||
} header: {
|
||||
if let title = context.viewState.usersSection.title {
|
||||
Text(title)
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(isDisabled: context.viewState.isUserDisabled(user),
|
||||
accessory: .selection(isSelected: context.viewState.isUserSelected(user))))
|
||||
}
|
||||
} header: {
|
||||
if let title = context.viewState.usersSection.title {
|
||||
Text(title)
|
||||
}
|
||||
.listRowSeparator(.automatic)
|
||||
.formSectionStyle()
|
||||
} else {
|
||||
Section.empty
|
||||
}
|
||||
.listRowSeparator(.automatic)
|
||||
.formSectionStyle()
|
||||
}
|
||||
|
||||
@State private var frame: CGRect = .zero
|
||||
@ScaledMetric private var cellWidth: CGFloat = 64
|
||||
@ScaledMetric private var cellWidth: CGFloat = 72
|
||||
|
||||
private var selectedUsersSection: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
ScrollViewReader { scrollView in
|
||||
HStack(spacing: 28) {
|
||||
HStack(spacing: 16) {
|
||||
ForEach(context.viewState.selectedUsers, id: \.userID) { user in
|
||||
InviteUsersScreenSelectedItem(user: user, imageProvider: context.imageProvider) {
|
||||
deselect(user)
|
||||
@@ -109,7 +115,7 @@ struct InviteUsersScreen: View {
|
||||
scrollView.scrollTo(id)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.horizontal, 14)
|
||||
}
|
||||
}
|
||||
.frame(width: frame.width)
|
||||
|
||||
+34
-14
@@ -21,30 +21,50 @@ struct InviteUsersScreenSelectedItem: View {
|
||||
let imageProvider: ImageProviderProtocol?
|
||||
let dismissAction: () -> Void
|
||||
|
||||
@ScaledMetric private var buttonSize: CGFloat = 20
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 4) {
|
||||
LoadableAvatarImage(url: user.avatarURL,
|
||||
name: user.displayName,
|
||||
contentID: user.userID,
|
||||
avatarSize: .user(on: .inviteUsers),
|
||||
imageProvider: imageProvider)
|
||||
VStack(spacing: 0) {
|
||||
avatar
|
||||
|
||||
Text(user.displayName ?? user.userID)
|
||||
.font(.compound.headingSM)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.overlay(alignment: .topTrailing) {
|
||||
Button(action: dismissAction) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(Color.element.systemPrimaryBackground, Color.element.primaryContent)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
var avatar: some View {
|
||||
LoadableAvatarImage(url: user.avatarURL,
|
||||
name: user.displayName,
|
||||
contentID: user.userID,
|
||||
avatarSize: .user(on: .inviteUsers),
|
||||
imageProvider: imageProvider)
|
||||
.overlay(alignment: .topTrailing) {
|
||||
Button(action: dismissAction) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: buttonSize, height: buttonSize)
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(Color.element.systemPrimaryBackground, Color.element.primaryContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct InviteUsersScreenSelectedItem_Previews: PreviewProvider {
|
||||
static let people: [UserProfileProxy] = [.mockAlice, .mockVerbose]
|
||||
|
||||
static var previews: some View {
|
||||
InviteUsersScreenSelectedItem(user: .mockAlice, imageProvider: MockMediaProvider(), dismissAction: { })
|
||||
ScrollView(.horizontal) {
|
||||
HStack(spacing: 28) {
|
||||
ForEach(Self.people, id: \.userID) { user in
|
||||
InviteUsersScreenSelectedItem(user: user, imageProvider: MockMediaProvider(), dismissAction: { })
|
||||
.frame(width: 72)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ struct InvitesScreen: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
if let rooms = context.viewState.invites, !rooms.isEmpty {
|
||||
LazyVStack {
|
||||
LazyVStack(spacing: 0) {
|
||||
ForEach(rooms, id: \.roomDetails.id) { invite in
|
||||
InvitesScreenCell(invite: invite,
|
||||
imageProvider: context.imageProvider,
|
||||
@@ -34,6 +34,7 @@ struct InvitesScreen: View {
|
||||
noInvitesContent
|
||||
}
|
||||
}
|
||||
.background(Color.element.background.ignoresSafeArea())
|
||||
.navigationTitle(L10n.actionInvitesList)
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ struct InvitesScreenCell: View {
|
||||
let acceptAction: () -> Void
|
||||
let declineAction: () -> Void
|
||||
|
||||
private let verticalInsets = 16.0
|
||||
@ScaledMetric private var badgeSize = 12.0
|
||||
|
||||
var body: some View {
|
||||
@@ -37,21 +36,23 @@ struct InvitesScreenCell: View {
|
||||
|
||||
mainContent
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.bottom, verticalInsets)
|
||||
.padding(.bottom, 16)
|
||||
.padding(.trailing, 16)
|
||||
.overlay(alignment: .bottom) {
|
||||
separator
|
||||
}
|
||||
}
|
||||
.padding(.top, verticalInsets)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 12)
|
||||
.padding(.leading, 16)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
private var mainContent: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 16) {
|
||||
textualContent
|
||||
.padding(.trailing, invite.isUnread ? 0 : 16)
|
||||
|
||||
if invite.isUnread {
|
||||
badge
|
||||
@@ -59,26 +60,28 @@ struct InvitesScreenCell: View {
|
||||
}
|
||||
|
||||
inviterView
|
||||
.padding(.top, 6)
|
||||
.padding(.trailing, 16)
|
||||
|
||||
buttons
|
||||
.padding(.trailing, 26)
|
||||
.padding(.top, 8)
|
||||
.padding(.top, 14)
|
||||
.padding(.trailing, 22)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var inviterView: some View {
|
||||
if let invitedText = attributedInviteText, let name = invite.inviter?.displayName {
|
||||
HStack {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
LoadableAvatarImage(url: invite.inviter?.avatarURL,
|
||||
name: name,
|
||||
contentID: name,
|
||||
avatarSize: .custom(16),
|
||||
imageProvider: imageProvider)
|
||||
.alignmentGuide(.firstTextBaseline) { $0[.bottom] * 0.8 }
|
||||
|
||||
Text(invitedText)
|
||||
}
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +91,7 @@ struct InvitesScreenCell: View {
|
||||
Text(title)
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.lineLimit(2)
|
||||
|
||||
if let subtitle {
|
||||
Text(subtitle)
|
||||
@@ -130,16 +134,21 @@ struct InvitesScreenCell: View {
|
||||
}
|
||||
|
||||
private var attributedInviteText: AttributedString? {
|
||||
guard invite.roomDetails.isDirect == false, let inviterName = invite.inviter?.displayName else {
|
||||
guard
|
||||
invite.roomDetails.isDirect == false,
|
||||
let inviterName = invite.inviter?.displayName,
|
||||
let inviterID = invite.inviter?.userID
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let text = L10n.screenInvitesInvitedYou(inviterName)
|
||||
let text = L10n.screenInvitesInvitedYou(inviterName, inviterID)
|
||||
var attributedString = AttributedString(text)
|
||||
attributedString.font = .compound.bodyMD
|
||||
attributedString.foregroundColor = .compound.textPlaceholder
|
||||
if let range = attributedString.range(of: inviterName) {
|
||||
attributedString[range].foregroundColor = .compound.textPrimary
|
||||
attributedString[range].font = .compound.bodyMDSemibold
|
||||
}
|
||||
return attributedString
|
||||
}
|
||||
@@ -153,18 +162,21 @@ struct InvitesScreenCell: View {
|
||||
|
||||
struct InvitesScreenCell_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
InvitesScreenCell(invite: .dm, imageProvider: MockMediaProvider(), acceptAction: { }, declineAction: { })
|
||||
.previewDisplayName("Direct room")
|
||||
|
||||
InvitesScreenCell(invite: .room(alias: nil), imageProvider: MockMediaProvider(), acceptAction: { }, declineAction: { })
|
||||
.previewDisplayName("Default room")
|
||||
|
||||
InvitesScreenCell(invite: .room(alias: "#footest:somewhere.org"), imageProvider: MockMediaProvider(), acceptAction: { }, declineAction: { })
|
||||
.previewDisplayName("Aliased room")
|
||||
|
||||
InvitesScreenCell(invite: .room(alias: "#footest:somewhere.org"), imageProvider: MockMediaProvider(), acceptAction: { }, declineAction: { })
|
||||
.dynamicTypeSize(.accessibility1)
|
||||
.previewDisplayName("Aliased room (AX1)")
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
InvitesScreenCell(invite: .dm, imageProvider: MockMediaProvider(), acceptAction: { }, declineAction: { })
|
||||
|
||||
InvitesScreenCell(invite: .room(), imageProvider: MockMediaProvider(), acceptAction: { }, declineAction: { })
|
||||
|
||||
InvitesScreenCell(invite: .room(isUnread: false), imageProvider: MockMediaProvider(), acceptAction: { }, declineAction: { })
|
||||
|
||||
InvitesScreenCell(invite: .room(alias: "#footest:somewhere.org", avatarURL: .picturesDirectory), imageProvider: MockMediaProvider(), acceptAction: { }, declineAction: { })
|
||||
|
||||
InvitesScreenCell(invite: .room(alias: "#footest:somewhere.org"), imageProvider: MockMediaProvider(), acceptAction: { }, declineAction: { })
|
||||
.dynamicTypeSize(.accessibility1)
|
||||
.previewDisplayName("Aliased room (AX1)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,20 +198,20 @@ private extension InvitesScreenRoomDetails {
|
||||
return .init(roomDetails: dmRoom, inviter: inviter, isUnread: false)
|
||||
}
|
||||
|
||||
static func room(alias: String?) -> InvitesScreenRoomDetails {
|
||||
static func room(alias: String? = nil, avatarURL: URL? = nil, isUnread: Bool = true) -> InvitesScreenRoomDetails {
|
||||
let dmRoom = RoomSummaryDetails(id: "@someone:somewhere.com",
|
||||
name: "Awesome Room",
|
||||
isDirect: false,
|
||||
avatarURL: nil,
|
||||
avatarURL: avatarURL,
|
||||
lastMessage: nil,
|
||||
lastMessageFormattedTimestamp: nil,
|
||||
unreadNotificationCount: 0,
|
||||
canonicalAlias: alias)
|
||||
let inviter = RoomMemberProxyMock()
|
||||
inviter.displayName = "Luca"
|
||||
inviter.userID = "@jack:somewhere.com"
|
||||
inviter.avatarURL = nil
|
||||
inviter.userID = "@jack:somewhi.nl"
|
||||
inviter.avatarURL = avatarURL
|
||||
|
||||
return .init(roomDetails: dmRoom, inviter: inviter, isUnread: true)
|
||||
return .init(roomDetails: dmRoom, inviter: inviter, isUnread: isUnread)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,36 +26,35 @@ struct RoomDetailsEditScreen: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
mainContent
|
||||
.elementFormStyle()
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.navigationTitle(L10n.screenRoomDetailsEditRoomTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(L10n.actionCancel) {
|
||||
context.send(viewAction: .cancel)
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(L10n.actionSave) {
|
||||
context.send(viewAction: .save)
|
||||
focus = nil
|
||||
}
|
||||
.disabled(!context.viewState.canSave)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var mainContent: some View {
|
||||
Form {
|
||||
avatar
|
||||
nameSection
|
||||
topicSection
|
||||
}
|
||||
.elementFormStyle()
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.navigationTitle(L10n.screenRoomDetailsEditRoomTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbar }
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(L10n.actionCancel) {
|
||||
context.send(viewAction: .cancel)
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(L10n.actionSave) {
|
||||
context.send(viewAction: .save)
|
||||
focus = nil
|
||||
}
|
||||
.disabled(!context.viewState.canSave)
|
||||
}
|
||||
}
|
||||
|
||||
private var avatar: some View {
|
||||
@@ -89,7 +88,7 @@ struct RoomDetailsEditScreen: View {
|
||||
|
||||
TextField(L10n.commonRoomName,
|
||||
text: $context.name,
|
||||
prompt: canEditName ? Text(L10n.screenCreateRoomRoomNamePlaceholder) : nil,
|
||||
prompt: canEditName ? Text(L10n.commonRoomNamePlaceholder) : nil,
|
||||
axis: .horizontal)
|
||||
.focused($focus, equals: .name)
|
||||
.font(.compound.bodyLG)
|
||||
@@ -110,7 +109,7 @@ struct RoomDetailsEditScreen: View {
|
||||
|
||||
TextField(L10n.commonTopic,
|
||||
text: $context.topic,
|
||||
prompt: canEditTopic ? Text(L10n.screenCreateRoomTopicPlaceholder) : nil,
|
||||
prompt: canEditTopic ? Text(L10n.commonTopicPlaceholder) : nil,
|
||||
axis: .vertical)
|
||||
.focused($focus, equals: .topic)
|
||||
.font(.compound.bodyLG)
|
||||
@@ -127,7 +126,8 @@ struct RoomDetailsEditScreen: View {
|
||||
|
||||
private var avatarOverlayIcon: some View {
|
||||
Image(systemName: "camera")
|
||||
.padding(2)
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.padding(3)
|
||||
.imageScale(.small)
|
||||
.foregroundColor(.white)
|
||||
.background {
|
||||
|
||||
@@ -153,7 +153,7 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie
|
||||
|
||||
private func showLoadingIndicator() {
|
||||
userIndicatorController?.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||
type: .modal,
|
||||
type: .modal(interactiveDismissDisabled: true),
|
||||
title: L10n.commonLoading,
|
||||
persistent: true))
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ struct StartChatScreen: View {
|
||||
}
|
||||
}
|
||||
.elementFormStyle()
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.navigationTitle(L10n.actionStartChat)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
@@ -64,7 +65,8 @@ struct StartChatScreen: View {
|
||||
private var createRoomSection: some View {
|
||||
Section {
|
||||
Button(action: createRoom) {
|
||||
Label(L10n.actionCreateARoom, systemImage: "person.3")
|
||||
Label(L10n.screenCreateRoomActionCreateRoom, systemImage: "person.3")
|
||||
.imageScale(.small)
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(accessory: .navigationLink))
|
||||
}
|
||||
@@ -99,6 +101,8 @@ struct StartChatScreen: View {
|
||||
}
|
||||
.listRowSeparator(.automatic)
|
||||
.formSectionStyle()
|
||||
} else {
|
||||
Section.empty
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ extension NotificationItemProxyProtocol {
|
||||
senderID: event.senderID,
|
||||
senderName: senderDisplayName ?? event.senderID,
|
||||
iconType: iconType)
|
||||
notification.body = L10n.screenInvitesInvitedYou("")
|
||||
notification.body = L10n.stateEventRoomInviteYou("")
|
||||
return notification
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ final class UserDiscoveryService: UserDiscoveryServiceProtocol {
|
||||
func searchProfiles(with searchQuery: String) async -> Result<[UserProfileProxy], UserDiscoveryErrorType> {
|
||||
do {
|
||||
async let queriedProfile = try? profileIfPossible(with: searchQuery).get()
|
||||
async let searchedUsers = clientProxy.searchUsers(searchTerm: searchQuery, limit: 5)
|
||||
async let searchedUsers = clientProxy.searchUsers(searchTerm: searchQuery, limit: 10)
|
||||
let users = try await merge(searchQuery: searchQuery, queriedProfile: queriedProfile, searchResults: searchedUsers.get())
|
||||
return .success(users)
|
||||
} catch {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
struct UserProfileProxy {
|
||||
struct UserProfileProxy: Equatable {
|
||||
let userID: String
|
||||
let displayName: String?
|
||||
let avatarURL: URL?
|
||||
|
||||
Reference in New Issue
Block a user