Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Join room by address #3840

Merged
merged 10 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 24 additions & 32 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@
"screen_security_and_privacy_room_publishing_section_header" = "Room publishing";
"screen_security_and_privacy_room_visibility_section_footer" = "Room addresses are ways to find and access rooms. This also ensures you can easily share your room with others.\nThe address is also required to make the room visible in %1$@ public room directory.";
"screen_security_and_privacy_title" = "Security & privacy";
"screen_start_chat_join_room_by_address_action" = "Join room by address";
"screen_start_chat_join_room_by_address_invalid_address" = "Not a valid address";
"screen_start_chat_join_room_by_address_placeholder" = "Enter...";
"screen_start_chat_join_room_by_address_room_found" = "Matching room found";
Expand All @@ -488,7 +489,6 @@
"screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed.";
"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices.";
"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices.";
"screen.start_chat.join_room_by_address_action" = "Join room by address";
"screen_account_provider_form_hint" = "Homeserver address";
"screen_account_provider_form_notice" = "Enter a search term or a domain address.";
"screen_account_provider_form_subtitle" = "Search for a company, community, or private server.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@
"screen_security_and_privacy_room_publishing_section_header" = "Room publishing";
"screen_security_and_privacy_room_visibility_section_footer" = "Room addresses are ways to find and access rooms. This also ensures you can easily share your room with others.\nThe address is also required to make the room visible in %1$@ public room directory.";
"screen_security_and_privacy_title" = "Security & privacy";
"screen_start_chat_join_room_by_address_action" = "Join room by address";
"screen_start_chat_join_room_by_address_invalid_address" = "Not a valid address";
"screen_start_chat_join_room_by_address_placeholder" = "Enter...";
"screen_start_chat_join_room_by_address_room_found" = "Matching room found";
Expand All @@ -488,7 +489,6 @@
"screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed.";
"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices.";
"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices.";
"screen.start_chat.join_room_by_address_action" = "Join room by address";
"screen_account_provider_form_hint" = "Homeserver address";
"screen_account_provider_form_notice" = "Enter a search term or a domain address.";
"screen_account_provider_form_subtitle" = "Search for a company, community, or private server.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -509,8 +509,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.processEvent(.showStartChatScreen)
case .presentGlobalSearch:
presentGlobalSearch()
case .presentRoomDirectorySearch:
stateMachine.processEvent(.showRoomDirectorySearchScreen)
case .logoutWithoutConfirmation:
self.actionsSubject.send(.logout)
case .logout:
Expand Down Expand Up @@ -654,10 +652,13 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
guard let self else { return }
switch action {
case .close:
self.navigationSplitCoordinator.setSheetCoordinator(nil)
navigationSplitCoordinator.setSheetCoordinator(nil)
case .openRoom(let roomID):
self.navigationSplitCoordinator.setSheetCoordinator(nil)
self.stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .room))
navigationSplitCoordinator.setSheetCoordinator(nil)
stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .room))
case .openRoomDirectorySearch:
navigationSplitCoordinator.setSheetCoordinator(nil)
stateMachine.processEvent(.showRoomDirectorySearchScreen)
}
}
.store(in: &cancellables)
Expand All @@ -668,9 +669,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
self?.stateMachine.processEvent(.dismissedStartChatScreen)
}
}

// MARK: Session Verification


// MARK: Calls

private func presentCallScreen(genericCallLink url: URL) {
Expand Down
9 changes: 2 additions & 7 deletions ElementX/Sources/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2460,6 +2460,8 @@ internal enum L10n {
internal static var screenSignoutSaveRecoveryKeyTitle: String { return L10n.tr("Localizable", "screen_signout_save_recovery_key_title") }
/// An error occurred when trying to start a chat
internal static var screenStartChatErrorStartingChat: String { return L10n.tr("Localizable", "screen_start_chat_error_starting_chat") }
/// Join room by address
internal static var screenStartChatJoinRoomByAddressAction: String { return L10n.tr("Localizable", "screen_start_chat_join_room_by_address_action") }
/// Not a valid address
internal static var screenStartChatJoinRoomByAddressInvalidAddress: String { return L10n.tr("Localizable", "screen_start_chat_join_room_by_address_invalid_address") }
/// Enter...
Expand Down Expand Up @@ -2874,13 +2876,6 @@ internal enum L10n {
/// You
internal static var you: String { return L10n.tr("Localizable", "common.you") }
}

internal enum Screen {
internal enum StartChat {
/// Join room by address
internal static var joinRoomByAddressAction: String { return L10n.tr("Localizable", "screen.start_chat.join_room_by_address_action") }
}
}
}
// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
Expand Down
214 changes: 214 additions & 0 deletions ElementX/Sources/Other/SwiftUI/Styles/ElementTextFieldStyle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//

import Compound
import SwiftUI
import SwiftUIIntrospect

extension TextFieldStyle where Self == ElementTextFieldStyle {
static func element(labelText: String? = nil,
footerText: String? = nil,
state: ElementTextFieldStyle.State = .default,
accessibilityIdentifier: String? = nil) -> ElementTextFieldStyle {
ElementTextFieldStyle(labelText: labelText.map(Text.init),
footerText: footerText.map(Text.init),
state: state,
accessibilityIdentifier: accessibilityIdentifier)
}

@_disfavoredOverload
static func element(labelText: Text? = nil,
footerText: Text? = nil,
state: ElementTextFieldStyle.State = .default,
accessibilityIdentifier: String? = nil) -> ElementTextFieldStyle {
ElementTextFieldStyle(labelText: labelText,
footerText: footerText,
state: state,
accessibilityIdentifier: accessibilityIdentifier)
}
}

/// The text field style used in authentication screens.
struct ElementTextFieldStyle: TextFieldStyle {
enum State {
case success
case error
case `default`
}

@Environment(\.isEnabled) private var isEnabled

@FocusState private var isFocused: Bool
let labelText: Text?
let footerText: Text?
let state: State
let accessibilityIdentifier: String?

private var isError: Bool {
state == .error
}

/// The color of the text field's border.
private var borderColor: Color {
isError ? .compound.textCriticalPrimary : .compound._borderTextFieldFocused
}

/// The width of the text field's border.
private var borderWidth: CGFloat {
isFocused || isError ? 1.0 : 0
}

private var accentColor: Color {
isError ? .compound.textCriticalPrimary : .compound.iconAccentTertiary
}

/// The color of the text inside the text field.
private var textColor: Color {
isEnabled ? .compound.textPrimary : .compound.textDisabled
}

/// The color of the text field's background.
private var backgroundColor: Color {
isError ? .compound.bgCriticalSubtleHovered :
.compound.bgSubtleSecondary.opacity(isEnabled ? 1 : 0.5)
}

/// The color of the placeholder text inside the text field.
private var placeholderColor: UIColor {
.compound.textSecondary
}

/// The color of the label above the text field.
private var labelColor: Color {
isEnabled ? .compound.textPrimary : .compound.textDisabled
}

/// The color of the footer label below the text field.
private var footerTextColor: Color {
switch state {
case .default:
.compound.textSecondary
case .error:
.compound.textCriticalPrimary
case .success:
.compound.textSuccessPrimary
}
}

private var footerIconColor: Color {
switch state {
// Doesn't matter we don't render it
case .default:
.clear
case .error:
.compound.iconCriticalPrimary
case .success:
.compound.iconSuccessPrimary
}
}

/// Creates the text field style configured as required.
/// - Parameters:
/// - labelText: The text shown in the label above the field.
/// - footerText: The text shown in the footer label below the field.
/// - isError: Whether or not the text field is currently in the error state.
init(labelText: Text? = nil, footerText: Text? = nil, state: State = .default, accessibilityIdentifier: String? = nil) {
self.labelText = labelText
self.footerText = footerText
self.state = state
self.accessibilityIdentifier = accessibilityIdentifier
}

@MainActor
func _body(configuration: TextField<_Label>) -> some View {
let rectangle = RoundedRectangle(cornerRadius: 14.0)

return VStack(alignment: .leading, spacing: 8) {
labelText
.font(.compound.bodySMSemibold)
.foregroundColor(labelColor)
.padding(.horizontal, 16)

configuration
.focused($isFocused)
.font(.compound.bodyLG)
.foregroundColor(textColor)
.accentColor(accentColor)
.padding(.leading, 16.0)
.padding([.vertical, .trailing], 11.0)
.background {
ZStack {
backgroundColor
.clipShape(rectangle)
rectangle
.stroke(borderColor, lineWidth: borderWidth)
}
.onTapGesture { isFocused = true } // Set focus with taps outside of the text field
}
.introspect(.textField, on: .supportedVersions) { textField in
textField.clearButtonMode = .whileEditing
textField.attributedPlaceholder = NSAttributedString(string: textField.placeholder ?? "",
attributes: [NSAttributedString.Key.foregroundColor: placeholderColor])
textField.accessibilityIdentifier = accessibilityIdentifier
}

if let footerText {
Label {
footerText
.tint(.compound.textLinkExternal)
.font(.compound.bodySM)
.foregroundColor(footerTextColor)
} icon: {
switch state {
case .success:
CompoundIcon(\.checkCircleSolid, size: .xSmall, relativeTo: .compound.bodySM)
.foregroundStyle(.compound.iconSuccessPrimary)
case .error:
CompoundIcon(\.errorSolid, size: .xSmall, relativeTo: .compound.bodySM)
.foregroundStyle(.compound.iconCriticalPrimary)
case .default:
EmptyView()
}
}
.labelStyle(.custom(spacing: 4, alignment: .top))
.padding(.horizontal, 16)
}
}
}
}

struct ElementTextFieldStyle_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
VStack(spacing: 20) {
// Plain text field.
TextField("Placeholder", text: .constant(""))
.textFieldStyle(.element())
TextField("Placeholder", text: .constant("Web"))
.textFieldStyle(.element())
TextField("Placeholder", text: .constant("Web"))
.textFieldStyle(.element())
.disabled(true)
TextField("Placeholder", text: .constant("Web"))
.textFieldStyle(.element(state: .error))

// Text field with labels
TextField("Placeholder", text: .constant(""))
.textFieldStyle(.element(labelText: "Label", footerText: "Footer"))
TextField("Placeholder", text: .constant("Input text"))
.textFieldStyle(.element(labelText: "Title", footerText: "Footer"))
TextField("Placeholder", text: .constant("Bad text"))
.textFieldStyle(.element(labelText: "Title", footerText: "Footer", state: .error))
TextField("Placeholder", text: .constant(""))
.textFieldStyle(.element(labelText: "Title", footerText: "Footer"))
.disabled(true)
TextField("Placeholder", text: .constant(""))
.textFieldStyle(.element(labelText: "Title", footerText: "Footer", state: .success))
}
.previewLayout(.sizeThatFits)
.padding()
}
}
Loading
Loading