mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Design review for first public TestFlight (#492)
* 430 - Design review for first public TestFlight
This commit is contained in:
@@ -25,10 +25,10 @@ internal enum Asset {
|
||||
internal enum Images {
|
||||
internal static let analyticsCheckmark = ImageAsset(name: "Images/AnalyticsCheckmark")
|
||||
internal static let analyticsLogo = ImageAsset(name: "Images/AnalyticsLogo")
|
||||
internal static let onboardingScreenPage1 = ImageAsset(name: "Images/Onboarding Screen Page 1")
|
||||
internal static let onboardingScreenPage2 = ImageAsset(name: "Images/Onboarding Screen Page 2")
|
||||
internal static let onboardingScreenPage3 = ImageAsset(name: "Images/Onboarding Screen Page 3")
|
||||
internal static let onboardingScreenPage4 = ImageAsset(name: "Images/Onboarding Screen Page 4")
|
||||
internal static let onboardingAppLogo = ImageAsset(name: "Images/Onboarding App Logo")
|
||||
internal static let onboardingBackgroundPart1 = ImageAsset(name: "Images/Onboarding Background Part 1")
|
||||
internal static let onboardingBackgroundPart2 = ImageAsset(name: "Images/Onboarding Background Part 2")
|
||||
internal static let onboardingBackgroundPart3 = ImageAsset(name: "Images/Onboarding Background Part 3")
|
||||
internal static let serverSelectionIcon = ImageAsset(name: "Images/Server Selection Icon")
|
||||
internal static let encryptionNormal = ImageAsset(name: "Images/encryption_normal")
|
||||
internal static let encryptionTrusted = ImageAsset(name: "Images/encryption_trusted")
|
||||
|
||||
@@ -16,10 +16,26 @@ extension ElementL10n {
|
||||
public static let actionConfirm = ElementL10n.tr("Untranslated", "action_confirm")
|
||||
/// Match
|
||||
public static let actionMatch = ElementL10n.tr("Untranslated", "action_match")
|
||||
/// Please describe the bug. What did you do? What did you expect to happen? What actually happened. Please go into as much detail as you can.
|
||||
public static let bugReportScreenDescription = ElementL10n.tr("Untranslated", "bug_report_screen_description")
|
||||
/// Send logs to help
|
||||
public static let bugReportScreenIncludeLogs = ElementL10n.tr("Untranslated", "bug_report_screen_include_logs")
|
||||
/// To check things work as intended, logs will be sent with your message. These will be private. To just send your message, turn off this setting.
|
||||
public static let bugReportScreenLogsDescription = ElementL10n.tr("Untranslated", "bug_report_screen_logs_description")
|
||||
/// Report a bug
|
||||
public static let bugReportScreenTitle = ElementL10n.tr("Untranslated", "bug_report_screen_title")
|
||||
/// %@ iOS
|
||||
public static func defaultSessionDisplayName(_ p1: Any) -> String {
|
||||
return ElementL10n.tr("Untranslated", "default_session_display_name", String(describing: p1))
|
||||
}
|
||||
/// Welcome to the %@ Beta. Supercharged, for speed and simplicity.
|
||||
public static func ftueAuthCarouselWelcomeBody(_ p1: Any) -> String {
|
||||
return ElementL10n.tr("Untranslated", "ftue_auth_carousel_welcome_body", String(describing: p1))
|
||||
}
|
||||
/// Be in your Element
|
||||
public static let ftueAuthCarouselWelcomeTitle = ElementL10n.tr("Untranslated", "ftue_auth_carousel_welcome_title")
|
||||
/// Enter your details
|
||||
public static let ftueAuthSignInEnterDetails = ElementL10n.tr("Untranslated", "ftue_auth_sign_in_enter_details")
|
||||
/// Mobile
|
||||
public static let loginMobileDevice = ElementL10n.tr("Untranslated", "login_mobile_device")
|
||||
/// Tablet
|
||||
@@ -70,6 +86,8 @@ extension ElementL10n {
|
||||
public static let notification = ElementL10n.tr("Untranslated", "Notification")
|
||||
/// About
|
||||
public static let roomDetailsAboutSectionTitle = ElementL10n.tr("Untranslated", "room_details_about_section_title")
|
||||
/// Copy Link
|
||||
public static let roomDetailsCopyLink = ElementL10n.tr("Untranslated", "room_details_copy_link")
|
||||
/// Info
|
||||
public static let roomDetailsTitle = ElementL10n.tr("Untranslated", "room_details_title")
|
||||
/// Failed loading messages
|
||||
@@ -96,7 +114,7 @@ extension ElementL10n {
|
||||
public static let screenshotDetectedMessage = ElementL10n.tr("Untranslated", "screenshot_detected_message")
|
||||
/// You took a screenshot
|
||||
public static let screenshotDetectedTitle = ElementL10n.tr("Untranslated", "screenshot_detected_title")
|
||||
/// You can only connect to a server that has already been set up
|
||||
/// You can only connect to an existing server
|
||||
public static let serverSelectionServerFooter = ElementL10n.tr("Untranslated", "server_selection_server_footer")
|
||||
/// Looks like you’re using a new device. Verify its you.
|
||||
public static let sessionVerificationBannerMessage = ElementL10n.tr("Untranslated", "session_verification_banner_message")
|
||||
|
||||
@@ -56,7 +56,7 @@ enum UserAvatarSizeOnScreen {
|
||||
case .settings:
|
||||
return 60
|
||||
case .roomDetails:
|
||||
return 32
|
||||
return 44
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ enum RoomAvatarSizeOnScreen {
|
||||
case .home:
|
||||
return 44
|
||||
case .details:
|
||||
return 100
|
||||
return 70
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// Copyright 2022 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
|
||||
|
||||
/// Style for section header
|
||||
struct FormSectionHeaderStyle: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.font(.element.footnoteBold)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
/// Applies the `FormSectionHeaderStyle` modifier to the view
|
||||
func formSectionHeader() -> some View {
|
||||
modifier(FormSectionHeaderStyle())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Small squared action button for settings screens
|
||||
struct SettingsActionButton: View {
|
||||
// MARK: Public
|
||||
|
||||
let title: String
|
||||
let image: Image
|
||||
let action: () -> Void
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@ScaledMetric private var menuIconSize = 54.0
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
VStack {
|
||||
image
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.frame(width: menuIconSize, height: menuIconSize)
|
||||
.background(RoundedRectangle(cornerRadius: 16).fill(Color.element.background))
|
||||
Text(title)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.font(.element.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Default row that can be reused for settings screens
|
||||
struct SettingsDefaultRow: View {
|
||||
// MARK: Public
|
||||
|
||||
let title: String
|
||||
let image: Image
|
||||
let action: () -> Void
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@ScaledMetric private var menuIconSize = 30.0
|
||||
private let listRowInsets = EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16)
|
||||
|
||||
// MARK: Views
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 16) {
|
||||
image
|
||||
.foregroundColor(.element.systemGray)
|
||||
.padding(4)
|
||||
.background(Color.element.systemGray6)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
.frame(width: menuIconSize, height: menuIconSize)
|
||||
|
||||
Text(title)
|
||||
.font(.element.body)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.forward")
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
}
|
||||
}
|
||||
.listRowInsets(listRowInsets)
|
||||
.listRowSeparator(.hidden)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
}
|
||||
}
|
||||
@@ -18,16 +18,17 @@ import SwiftUI
|
||||
|
||||
/// An image that is styled for use as the screen icon in the onboarding flow.
|
||||
struct AuthenticationIconImage: View {
|
||||
let image: ImageAsset
|
||||
let image: Image
|
||||
|
||||
var body: some View {
|
||||
Image(image.name)
|
||||
image
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.foregroundColor(.element.accent)
|
||||
.frame(width: 90, height: 90)
|
||||
.background(.white, in: Circle().inset(by: 2))
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.accessibilityHidden(true)
|
||||
.padding(20)
|
||||
.frame(width: 72, height: 72)
|
||||
.background(RoundedRectangle(cornerRadius: 14).fill(Color.element.quinaryContent))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +36,6 @@ struct AuthenticationIconImage: View {
|
||||
|
||||
struct AuthenticationIconImage_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AuthenticationIconImage(image: Asset.Images.serverSelectionIcon)
|
||||
AuthenticationIconImage(image: Image(asset: Asset.Images.serverSelectionIcon))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,13 +197,11 @@ final class LoginCoordinator: CoordinatorProtocol {
|
||||
|
||||
/// Presents the server selection screen as a modal.
|
||||
private func presentServerSelectionScreen() {
|
||||
let serverSelectionNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let userNotificationController = UserNotificationController(rootCoordinator: serverSelectionNavigationStackCoordinator)
|
||||
let userNotificationController = UserNotificationController(rootCoordinator: navigationStackCoordinator)
|
||||
|
||||
let parameters = ServerSelectionCoordinatorParameters(authenticationService: authenticationService,
|
||||
userNotificationController: userNotificationController,
|
||||
isModallyPresented: true)
|
||||
isModallyPresented: false)
|
||||
|
||||
let coordinator = ServerSelectionCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self, weak coordinator] action in
|
||||
@@ -211,9 +209,7 @@ final class LoginCoordinator: CoordinatorProtocol {
|
||||
self.serverSelectionCoordinator(coordinator, didCompleteWith: action)
|
||||
}
|
||||
|
||||
serverSelectionNavigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(userNotificationController)
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
/// Handles the result from the server selection modal, dismissing it after updating the view.
|
||||
@@ -223,7 +219,7 @@ final class LoginCoordinator: CoordinatorProtocol {
|
||||
updateViewModel()
|
||||
}
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
navigationStackCoordinator.pop()
|
||||
}
|
||||
|
||||
/// Shows the forgot password screen.
|
||||
|
||||
@@ -29,15 +29,10 @@ struct LoginScreen: View {
|
||||
VStack(spacing: 0) {
|
||||
header
|
||||
.padding(.top, UIConstants.topPaddingToNavigationBar)
|
||||
.padding(.bottom, 36)
|
||||
.padding(.bottom, 32)
|
||||
|
||||
serverInfo
|
||||
.padding(.leading, 12)
|
||||
|
||||
Rectangle()
|
||||
.fill(Color.element.quinaryContent)
|
||||
.frame(height: 1)
|
||||
.padding(.vertical, 21)
|
||||
.padding(.bottom, 32)
|
||||
|
||||
switch context.viewState.loginMode {
|
||||
case .password:
|
||||
@@ -59,7 +54,7 @@ struct LoginScreen: View {
|
||||
/// The header containing a Welcome Back title.
|
||||
var header: some View {
|
||||
Text(ElementL10n.ftueAuthWelcomeBackTitle)
|
||||
.font(.element.title2Bold)
|
||||
.font(.element.title1Bold)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
}
|
||||
@@ -73,7 +68,13 @@ struct LoginScreen: View {
|
||||
|
||||
/// The form with text fields for username and password, along with a submit button.
|
||||
var loginForm: some View {
|
||||
VStack(spacing: 14) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(ElementL10n.ftueAuthSignInEnterDetails)
|
||||
.font(.element.subheadline)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
TextField(ElementL10n.loginSigninUsernameHint, text: $context.username)
|
||||
.focused($isUsernameFocused)
|
||||
.textFieldStyle(.elementInput())
|
||||
@@ -84,8 +85,7 @@ struct LoginScreen: View {
|
||||
.onChange(of: isUsernameFocused, perform: usernameFocusChanged)
|
||||
.onSubmit { isPasswordFocused = true }
|
||||
.accessibilityIdentifier("usernameTextField")
|
||||
|
||||
Spacer().frame(height: 20)
|
||||
.padding(.bottom, 20)
|
||||
|
||||
SecureField(ElementL10n.loginSignupPasswordHint, text: $context.password)
|
||||
.focused($isPasswordFocused)
|
||||
@@ -95,15 +95,18 @@ struct LoginScreen: View {
|
||||
.onSubmit(submit)
|
||||
.accessibilityIdentifier("passwordTextField")
|
||||
|
||||
Button { context.send(viewAction: .forgotPassword) } label: {
|
||||
Text(ElementL10n.ftueAuthForgotPassword)
|
||||
.font(.element.body)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.padding(.bottom, 8)
|
||||
// uncomment this piece of code once forgot password will be available
|
||||
// Button { context.send(viewAction: .forgotPassword) } label: {
|
||||
// Text(ElementL10n.ftueAuthForgotPassword)
|
||||
// .font(.element.body)
|
||||
// }
|
||||
// .frame(maxWidth: .infinity, alignment: .trailing)
|
||||
// .padding(.bottom, 8)
|
||||
|
||||
Spacer().frame(height: 32)
|
||||
|
||||
Button(action: submit) {
|
||||
Text(ElementL10n.loginSignupSubmit)
|
||||
Text(ElementL10n.loginContinue)
|
||||
}
|
||||
.buttonStyle(.elementAction(.xLarge))
|
||||
.disabled(!context.viewState.canSubmit)
|
||||
|
||||
+25
-14
@@ -29,25 +29,36 @@ struct LoginServerInfoSection: View {
|
||||
// MARK: - Views
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(ElementL10n.ftueAuthSignInChooseServerHeader)
|
||||
.font(.element.subheadline)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
HStack {
|
||||
Text(address)
|
||||
.font(.element.body)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: editAction) {
|
||||
Text(ElementL10n.edit)
|
||||
.padding(.vertical, 2)
|
||||
Button(action: editAction) {
|
||||
HStack {
|
||||
Text(address)
|
||||
.font(.element.subheadlineBold)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
.padding(.trailing, 16)
|
||||
}
|
||||
.buttonStyle(.elementGhost())
|
||||
.accessibilityIdentifier("editServerButton")
|
||||
.background(RoundedRectangle(cornerRadius: 14).fill(Color.element.system))
|
||||
}
|
||||
.accessibilityIdentifier("editServerButton")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginServerInfoSection_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LoginServerInfoSection(address: "matrix.org", editAction: { })
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ struct ServerSelectionViewState: BindableState {
|
||||
|
||||
/// The title shown on the confirm button.
|
||||
var buttonTitle: String {
|
||||
isModallyPresented ? ElementL10n.actionConfirm : ElementL10n.actionNext
|
||||
isModallyPresented ? ElementL10n.continue : ElementL10n.actionNext
|
||||
}
|
||||
|
||||
/// The text field is showing an error.
|
||||
|
||||
+5
-5
@@ -40,7 +40,7 @@ struct ServerSelectionScreen: View {
|
||||
/// The title, message and icon at the top of the screen.
|
||||
var header: some View {
|
||||
VStack(spacing: 8) {
|
||||
AuthenticationIconImage(image: Asset.Images.serverSelectionIcon)
|
||||
AuthenticationIconImage(image: Image(asset: Asset.Images.serverSelectionIcon))
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Text(ElementL10n.ftueAuthChooseServerTitle)
|
||||
@@ -51,13 +51,14 @@ struct ServerSelectionScreen: View {
|
||||
Text(ElementL10n.ftueAuthChooseServerSubtitle)
|
||||
.font(.element.body)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.foregroundColor(.element.tertiaryContent)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
/// The text field and confirm button where the user enters a server URL.
|
||||
var serverForm: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
TextField(ElementL10n.ftueAuthChooseServerEntryHint, text: $context.homeserverAddress)
|
||||
.textFieldStyle(.elementInput(labelText: ElementL10n.hsUrl,
|
||||
footerText: context.viewState.footerMessage,
|
||||
@@ -70,8 +71,6 @@ struct ServerSelectionScreen: View {
|
||||
.onSubmit(submit)
|
||||
.accessibilityIdentifier("addressTextField")
|
||||
|
||||
Divider()
|
||||
|
||||
TextField(ElementL10n.ftueAuthChooseServerEntryHint, text: $context.slidingSyncProxyAddress)
|
||||
.textFieldStyle(.elementInput(labelText: "Sliding sync proxy URL"))
|
||||
.keyboardType(.URL)
|
||||
@@ -80,6 +79,7 @@ struct ServerSelectionScreen: View {
|
||||
.submitLabel(.done)
|
||||
.onSubmit(submit)
|
||||
.accessibilityIdentifier("slidingSyncProxyAddressTextField")
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Button(action: submit) {
|
||||
Text(context.viewState.buttonTitle)
|
||||
|
||||
@@ -38,6 +38,5 @@ struct BugReportViewStateBindings {
|
||||
enum BugReportViewAction {
|
||||
case cancel
|
||||
case submit
|
||||
case toggleSendLogs
|
||||
case removeScreenshot
|
||||
}
|
||||
|
||||
@@ -41,8 +41,6 @@ class BugReportViewModel: BugReportViewModelType, BugReportViewModelProtocol {
|
||||
callback?(.cancel)
|
||||
case .submit:
|
||||
await submitBugReport()
|
||||
case .toggleSendLogs:
|
||||
context.sendingLogsEnabled.toggle()
|
||||
case .removeScreenshot:
|
||||
state.screenshot = nil
|
||||
}
|
||||
|
||||
@@ -26,93 +26,81 @@ struct BugReportScreen: View {
|
||||
@ObservedObject var context: BugReportViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
VStack {
|
||||
ScrollView {
|
||||
mainContent
|
||||
.padding(.top, 50)
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
}
|
||||
.introspectScrollView { scrollView in
|
||||
scrollView.keyboardDismissMode = .onDrag
|
||||
}
|
||||
|
||||
buttons
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
|
||||
}
|
||||
.navigationTitle(ElementL10n.titleActivityBugReport)
|
||||
.toolbar {
|
||||
if context.viewState.isModallyPresented {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(ElementL10n.actionCancel) {
|
||||
context.send(viewAction: .cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.interactiveDismissDisabled()
|
||||
ScrollView {
|
||||
mainContent
|
||||
.padding(.top, 50)
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
}
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.background(Color.element.system)
|
||||
.navigationTitle(ElementL10n.bugReportScreenTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbar }
|
||||
.interactiveDismissDisabled()
|
||||
}
|
||||
|
||||
|
||||
/// The main content of the view to be shown in a scroll view.
|
||||
var mainContent: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text(ElementL10n.sendBugReportDescription)
|
||||
.accessibilityIdentifier("reportBugDescription")
|
||||
ZStack(alignment: .topLeading) {
|
||||
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
||||
.fill(Color.element.system)
|
||||
|
||||
if context.reportText.isEmpty {
|
||||
Text(ElementL10n.sendBugReportPlaceholder)
|
||||
.foregroundColor(Color.element.secondaryContent)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
TextEditor(text: $context.reportText)
|
||||
.padding(4)
|
||||
.background(Color.clear)
|
||||
.cornerRadius(8)
|
||||
.accessibilityIdentifier("reportTextView")
|
||||
.introspectTextView { textView in
|
||||
textView.backgroundColor = .clear
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 300)
|
||||
.font(.body)
|
||||
Text(ElementL10n.sendBugReportLogsDescription)
|
||||
.accessibilityIdentifier("sendLogsDescription")
|
||||
HStack(spacing: 12) {
|
||||
Toggle(ElementL10n.sendBugReportIncludeLogs, isOn: $context.sendingLogsEnabled)
|
||||
.toggleStyle(ElementToggleStyle())
|
||||
.accessibilityIdentifier("sendLogsToggle")
|
||||
Text(ElementL10n.sendBugReportIncludeLogs)
|
||||
.accessibilityIdentifier("sendLogsText")
|
||||
}
|
||||
.onTapGesture {
|
||||
context.send(viewAction: .toggleSendLogs)
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 24) {
|
||||
descriptionTextEditor
|
||||
sendLogsToggle
|
||||
screenshot
|
||||
}
|
||||
}
|
||||
|
||||
/// The action buttons shown at the bottom of the view.
|
||||
var buttons: some View {
|
||||
VStack {
|
||||
Button { context.send(viewAction: .submit) } label: {
|
||||
Text(ElementL10n.actionSend)
|
||||
@ViewBuilder
|
||||
private var descriptionTextEditor: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.fill(Color.element.background)
|
||||
|
||||
TextEditor(text: $context.reportText)
|
||||
.tint(.element.brand)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.clear)
|
||||
.cornerRadius(14)
|
||||
.accessibilityIdentifier("reportTextView")
|
||||
.introspectTextView { textView in
|
||||
textView.backgroundColor = .clear
|
||||
}
|
||||
|
||||
if context.reportText.isEmpty {
|
||||
Text(ElementL10n.bugReportScreenDescription)
|
||||
.font(.element.body)
|
||||
.foregroundColor(Color.element.secondaryContent)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.disabled(context.reportText.count < 5)
|
||||
.accessibilityIdentifier("sendButton")
|
||||
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.stroke(Color.element.quaternaryContent)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 300)
|
||||
.font(.body)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var sendLogsToggle: some View {
|
||||
VStack(spacing: 8) {
|
||||
Toggle(ElementL10n.bugReportScreenIncludeLogs, isOn: $context.sendingLogsEnabled)
|
||||
.tint(Color.element.brand)
|
||||
.accessibilityIdentifier("sendLogsToggle")
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 11)
|
||||
.background(RoundedRectangle(cornerRadius: 14).fill(Color.element.background))
|
||||
|
||||
Text(ElementL10n.bugReportScreenLogsDescription)
|
||||
.font(.element.caption1)
|
||||
.foregroundColor(Color.element.secondaryContent)
|
||||
.padding(.horizontal, -8)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
var screenshot: some View {
|
||||
private var screenshot: some View {
|
||||
if let screenshot = context.viewState.screenshot {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: screenshot)
|
||||
@@ -126,7 +114,27 @@ struct BugReportScreen: View {
|
||||
.offset(x: 10, y: -10)
|
||||
.accessibilityIdentifier("removeScreenshotButton")
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
.padding(.vertical, 16)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbar: some ToolbarContent {
|
||||
if context.viewState.isModallyPresented {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button(ElementL10n.actionCancel) {
|
||||
context.send(viewAction: .cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(ElementL10n.actionSend) {
|
||||
context.send(viewAction: .submit)
|
||||
}
|
||||
.disabled(context.reportText.count < 5)
|
||||
.accessibilityIdentifier("sendButton")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,23 +60,10 @@ struct OnboardingViewState: BindableState {
|
||||
}
|
||||
|
||||
init() {
|
||||
// The pun doesn't translate, so we only use it for English.
|
||||
let locale = Locale.current
|
||||
let page4Title = locale.identifier.hasPrefix("en") ? "Cut the slack from teams." : ElementL10n.ftueAuthCarouselWorkplaceTitle
|
||||
|
||||
content = [
|
||||
OnboardingPageContent(title: ElementL10n.ftueAuthCarouselSecureTitle.tinting("."),
|
||||
message: ElementL10n.ftueAuthCarouselSecureBody,
|
||||
image: Asset.Images.onboardingScreenPage1),
|
||||
OnboardingPageContent(title: ElementL10n.ftueAuthCarouselControlTitle.tinting("."),
|
||||
message: ElementL10n.ftueAuthCarouselControlBody,
|
||||
image: Asset.Images.onboardingScreenPage2),
|
||||
OnboardingPageContent(title: ElementL10n.ftueAuthCarouselEncryptedTitle.tinting("."),
|
||||
message: ElementL10n.ftueAuthCarouselEncryptedBody,
|
||||
image: Asset.Images.onboardingScreenPage3),
|
||||
OnboardingPageContent(title: page4Title.tinting("."),
|
||||
message: ElementL10n.ftueAuthCarouselWorkplaceBody(InfoPlistReader.target.bundleDisplayName),
|
||||
image: Asset.Images.onboardingScreenPage4)
|
||||
OnboardingPageContent(title: ElementL10n.ftueAuthCarouselWelcomeTitle.tinting(".", color: .element.accent),
|
||||
message: ElementL10n.ftueAuthCarouselWelcomeBody(InfoPlistReader.target.bundleDisplayName),
|
||||
image: Asset.Images.onboardingAppLogo)
|
||||
]
|
||||
bindings = OnboardingBindings()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// 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 DesignKit
|
||||
import SwiftUI
|
||||
|
||||
/// A glossy animated background view designed for the onboarding screen
|
||||
struct OnboardingBackgroundView: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
@State private var factor = 0.0
|
||||
@State private var isReversed = false
|
||||
|
||||
private let step = 0.001
|
||||
private let timer = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
Image(asset: Asset.Images.onboardingBackgroundPart1)
|
||||
.position(x: geometry.size.width * 0.125 - factor * geometry.size.width * 0.25, y: (1.0 - factor) * geometry.size.height * 0.55)
|
||||
Image(asset: Asset.Images.onboardingBackgroundPart2)
|
||||
.position(x: geometry.size.width * 1.05, y: factor * geometry.size.height * 0.45)
|
||||
Image(asset: Asset.Images.onboardingBackgroundPart3)
|
||||
.position(x: factor * geometry.size.width, y: geometry.size.height * 1.05 - factor * geometry.size.height * 0.08)
|
||||
}
|
||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||
.onReceive(timer) { _ in
|
||||
if isReversed {
|
||||
guard factor > 0 else {
|
||||
isReversed = false
|
||||
factor = step
|
||||
return
|
||||
}
|
||||
|
||||
factor -= step
|
||||
} else {
|
||||
guard factor < 1 else {
|
||||
isReversed = true
|
||||
factor = 1 - step
|
||||
return
|
||||
}
|
||||
|
||||
factor += 0.001
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingBackgroundView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
OnboardingBackgroundView()
|
||||
}
|
||||
}
|
||||
@@ -20,27 +20,38 @@ struct OnboardingPageView: View {
|
||||
/// The content that this page should display.
|
||||
let content: OnboardingPageContent
|
||||
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image(content.image.name)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: 310) // This value is problematic. 300 results in dropped frames
|
||||
// on iPhone 12/13 Mini. 305 the same on iPhone 12/13. As of
|
||||
// iOS 15, 310 seems fine on all supported screen widths 🤞.
|
||||
.padding(20)
|
||||
.accessibilityHidden(true)
|
||||
if verticalSizeClass == .regular {
|
||||
Spacer()
|
||||
|
||||
Image(content.image.name)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.padding(60)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Spacer()
|
||||
|
||||
Text(content.title)
|
||||
.font(.element.title2Bold)
|
||||
.font(.element.title1Bold)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.multilineTextAlignment(.center)
|
||||
Text(content.message)
|
||||
.font(.element.body)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding()
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.bottom)
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
@@ -21,7 +21,8 @@ import SwiftUI
|
||||
struct OnboardingScreen: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.layoutDirection) private var layoutDirection
|
||||
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
|
||||
private var isLeftToRight: Bool { layoutDirection == .leftToRight }
|
||||
private var pageCount: Int { context.viewState.content.count }
|
||||
|
||||
@@ -33,64 +34,68 @@ struct OnboardingScreen: View {
|
||||
@ObservedObject var context: OnboardingViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
.frame(height: UIConstants.spacerHeight(in: geometry))
|
||||
|
||||
// The main content of the carousel
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
// Add a hidden page at the start of the carousel duplicating the content of the last page
|
||||
OnboardingPageView(content: context.viewState.content[pageCount - 1])
|
||||
.frame(width: geometry.size.width)
|
||||
.accessibilityIdentifier("hiddenPage")
|
||||
|
||||
ForEach(0..<pageCount, id: \.self) { index in
|
||||
OnboardingPageView(content: context.viewState.content[index])
|
||||
ZStack {
|
||||
OnboardingBackgroundView()
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
.frame(height: UIConstants.spacerHeight(in: geometry))
|
||||
|
||||
// The main content of the carousel
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
// Add a hidden page at the start of the carousel duplicating the content of the last page
|
||||
OnboardingPageView(content: context.viewState.content[pageCount - 1])
|
||||
.frame(width: geometry.size.width)
|
||||
.accessibilityIdentifier("hiddenPage")
|
||||
|
||||
ForEach(0..<pageCount, id: \.self) { index in
|
||||
OnboardingPageView(content: context.viewState.content[index])
|
||||
.frame(width: geometry.size.width)
|
||||
}
|
||||
}
|
||||
.offset(x: pageOffset(in: geometry))
|
||||
|
||||
Spacer()
|
||||
|
||||
if pageCount > 1 {
|
||||
OnboardingPageIndicator(pageCount: pageCount, pageIndex: context.pageIndex)
|
||||
.frame(width: geometry.size.width)
|
||||
.padding(.bottom)
|
||||
}
|
||||
|
||||
buttons
|
||||
.frame(width: geometry.size.width)
|
||||
.padding(.bottom, UIConstants.actionButtonBottomPadding)
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
|
||||
|
||||
Spacer()
|
||||
.frame(height: UIConstants.spacerHeight(in: geometry))
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.gesture(
|
||||
DragGesture()
|
||||
.onChanged(handleDragGestureChange)
|
||||
.onEnded { handleDragGestureEnded($0, viewSize: geometry.size) }
|
||||
)
|
||||
}
|
||||
.offset(x: pageOffset(in: geometry))
|
||||
|
||||
Spacer()
|
||||
|
||||
OnboardingPageIndicator(pageCount: pageCount, pageIndex: context.pageIndex)
|
||||
.frame(width: geometry.size.width)
|
||||
.padding(.bottom)
|
||||
|
||||
Spacer()
|
||||
|
||||
buttons
|
||||
.frame(width: geometry.size.width)
|
||||
.padding(.bottom, UIConstants.actionButtonBottomPadding)
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
|
||||
|
||||
Spacer()
|
||||
.frame(height: UIConstants.spacerHeight(in: geometry))
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.background(background.ignoresSafeArea().offset(x: pageOffset(in: geometry)))
|
||||
.gesture(
|
||||
DragGesture()
|
||||
.onChanged(handleDragGestureChange)
|
||||
.onEnded { handleDragGestureEnded($0, viewSize: geometry.size) }
|
||||
)
|
||||
.navigationBarHidden(true)
|
||||
.onAppear(perform: startTimer)
|
||||
.onDisappear(perform: stopTimer)
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.onAppear(perform: startTimer)
|
||||
.onDisappear(perform: stopTimer)
|
||||
}
|
||||
|
||||
/// The main action buttons.
|
||||
var buttons: some View {
|
||||
VStack(spacing: 12) {
|
||||
Button { context.send(viewAction: .login) } label: {
|
||||
Text(ElementL10n.loginSplashSubmit)
|
||||
Text(ElementL10n.loginContinue)
|
||||
}
|
||||
.buttonStyle(.elementAction(.xLarge))
|
||||
.accessibilityIdentifier("getStartedButton")
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.horizontal, verticalSizeClass == .compact ? 128 : 24)
|
||||
.readableFrame()
|
||||
}
|
||||
|
||||
@@ -111,7 +116,7 @@ struct OnboardingScreen: View {
|
||||
|
||||
/// Starts the animation timer for an automatic carousel effect.
|
||||
private func startTimer() {
|
||||
guard pageTimer == nil else { return }
|
||||
guard pageTimer == nil, pageCount > 1 else { return }
|
||||
|
||||
pageTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
|
||||
if context.pageIndex == pageCount - 1 {
|
||||
|
||||
@@ -30,6 +30,7 @@ enum RoomDetailsViewModelAction {
|
||||
|
||||
struct RoomDetailsViewState: BindableState {
|
||||
let roomId: String
|
||||
let canonicalAlias: String?
|
||||
let isEncrypted: Bool
|
||||
let isDirect: Bool
|
||||
var title = ""
|
||||
@@ -56,6 +57,8 @@ enum RoomDetailsErrorType: Hashable {
|
||||
|
||||
enum RoomDetailsViewAction {
|
||||
case processTapPeople
|
||||
case copyRoomLink
|
||||
case inviteToRoom
|
||||
}
|
||||
|
||||
struct RoomDetailsMember: Identifiable, Equatable {
|
||||
|
||||
@@ -27,8 +27,10 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc
|
||||
|
||||
var callback: ((RoomDetailsViewModelAction) -> Void)?
|
||||
|
||||
init(roomProxy: RoomProxyProtocol, mediaProvider: MediaProviderProtocol) {
|
||||
init(roomProxy: RoomProxyProtocol,
|
||||
mediaProvider: MediaProviderProtocol) {
|
||||
super.init(initialViewState: .init(roomId: roomProxy.id,
|
||||
canonicalAlias: roomProxy.canonicalAlias,
|
||||
isEncrypted: roomProxy.isEncrypted,
|
||||
isDirect: roomProxy.isDirect,
|
||||
title: roomProxy.displayName ?? roomProxy.name ?? "Unknown Room",
|
||||
@@ -37,7 +39,7 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc
|
||||
members: [],
|
||||
bindings: .init()),
|
||||
imageProvider: mediaProvider)
|
||||
|
||||
|
||||
Task {
|
||||
switch await roomProxy.members() {
|
||||
case .success(let members):
|
||||
@@ -55,6 +57,21 @@ class RoomDetailsViewModel: RoomDetailsViewModelType, RoomDetailsViewModelProtoc
|
||||
switch viewAction {
|
||||
case .processTapPeople:
|
||||
callback?(.requestMemberDetailsPresentation(members))
|
||||
case .copyRoomLink:
|
||||
copyRoomLink()
|
||||
case .inviteToRoom:
|
||||
inviteToRoom()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func copyRoomLink() {
|
||||
// TODO: to be implemented
|
||||
// ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(title: ElementL10n.linkCopiedToClipboard))
|
||||
}
|
||||
|
||||
private func inviteToRoom() {
|
||||
// TODO: to be implemented
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,96 +37,121 @@ struct RoomDetailsScreen: View {
|
||||
}
|
||||
}
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
.navigationTitle(ElementL10n.roomDetailsTitle)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var headerSection: some View {
|
||||
VStack(spacing: 16.0) {
|
||||
VStack(spacing: 8.0) {
|
||||
LoadableAvatarImage(url: context.viewState.avatarURL,
|
||||
name: context.viewState.title,
|
||||
contentID: context.viewState.roomId,
|
||||
avatarSize: .room(on: .details),
|
||||
imageProvider: context.imageProvider)
|
||||
.accessibilityIdentifier("roomAvatarImage")
|
||||
|
||||
|
||||
Text(context.viewState.title)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.font(.element.headline)
|
||||
.font(.element.title1Bold)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
if let canonicalAlias = context.viewState.canonicalAlias {
|
||||
Text(canonicalAlias)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.font(.element.body)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
// TODO: uncomment this code to display copy link and invite buttons
|
||||
// HStack(spacing: 32) {
|
||||
// SettingsActionButton(title: ElementL10n.roomDetailsCopyLink, image: Image(systemName: "link")) {
|
||||
// context.send(viewAction: .copyRoomLink)
|
||||
// }
|
||||
// SettingsActionButton(title: ElementL10n.inviteUsersToRoomActionInvite.capitalized, image: Image(systemName: "square.and.arrow.up")) {
|
||||
// context.send(viewAction: .inviteToRoom)
|
||||
// }
|
||||
// }
|
||||
// .padding(.top, 32)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.listRowBackground(Color.clear)
|
||||
.padding(.top, 18)
|
||||
.padding(.bottom, 42)
|
||||
}
|
||||
|
||||
|
||||
private func topicSection(with topic: String) -> some View {
|
||||
Section(ElementL10n.roomSettingsTopic) {
|
||||
Text(topic)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.font(.element.footnote)
|
||||
}
|
||||
Section(header: Text(ElementL10n.roomSettingsTopic)
|
||||
.formSectionHeader()) {
|
||||
Text(topic)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.font(.element.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
private var aboutSection: some View {
|
||||
Section(ElementL10n.roomDetailsAboutSectionTitle) {
|
||||
Button {
|
||||
context.send(viewAction: .processTapPeople)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "person")
|
||||
.foregroundColor(.element.systemGray)
|
||||
.padding(4)
|
||||
.background(Color.element.systemGray6)
|
||||
.clipShape(Circle())
|
||||
.frame(width: menuIconSize, height: menuIconSize)
|
||||
Text(ElementL10n.bottomActionPeople)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.font(.body)
|
||||
Spacer()
|
||||
Section(header: Text(ElementL10n.roomDetailsAboutSectionTitle)
|
||||
.formSectionHeader()) {
|
||||
Button {
|
||||
context.send(viewAction: .processTapPeople)
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "person")
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.padding(4)
|
||||
.background(Color.element.system)
|
||||
.clipShape(RoundedCornerShape(radius: 8, corners: .allCorners))
|
||||
.frame(width: menuIconSize, height: menuIconSize)
|
||||
|
||||
if context.viewState.isLoadingMembers {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text(String(context.viewState.members.count))
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.font(.element.body)
|
||||
Image(systemName: "chevron.forward")
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
Text(ElementL10n.bottomActionPeople)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.font(.body)
|
||||
|
||||
Spacer()
|
||||
|
||||
if context.viewState.isLoadingMembers {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text(String(context.viewState.members.count))
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.font(.element.body)
|
||||
Image(systemName: "chevron.forward")
|
||||
.foregroundColor(.element.quaternaryContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowInsets(listRowInsets)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.accessibilityIdentifier("peopleButton")
|
||||
.disabled(context.viewState.isLoadingMembers)
|
||||
}
|
||||
.listRowInsets(listRowInsets)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.accessibilityIdentifier("peopleButton")
|
||||
.disabled(context.viewState.isLoadingMembers)
|
||||
}
|
||||
}
|
||||
|
||||
private var securitySection: some View {
|
||||
Section(ElementL10n.roomProfileSectionSecurity) {
|
||||
HStack(alignment: .center) {
|
||||
Image(systemName: "lock.shield")
|
||||
.foregroundColor(.element.systemGray)
|
||||
.padding(4)
|
||||
.background(Color.element.systemGray6)
|
||||
.clipShape(Circle())
|
||||
.frame(width: menuIconSize, height: menuIconSize)
|
||||
VStack(alignment: .leading) {
|
||||
Text(ElementL10n.encryptionEnabled)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.font(.element.body)
|
||||
Text(ElementL10n.encryptionEnabledTileDescription)
|
||||
Section(header: Text(ElementL10n.roomProfileSectionSecurity)
|
||||
.formSectionHeader()) {
|
||||
HStack(alignment: .top) {
|
||||
Image(systemName: "lock.shield")
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.font(.element.footnote)
|
||||
.padding(4)
|
||||
.background(Color.element.system)
|
||||
.clipShape(RoundedCornerShape(radius: 8, corners: .allCorners))
|
||||
.frame(width: menuIconSize, height: menuIconSize)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(ElementL10n.encryptionEnabled)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.font(.element.body)
|
||||
|
||||
Text(ElementL10n.encryptionEnabledTileDescription)
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.font(.element.footnote)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.element.quaternaryContent)
|
||||
}
|
||||
Spacer()
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.padding(.horizontal, -3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +169,7 @@ struct RoomDetails_Previews: PreviewProvider {
|
||||
topic: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
isDirect: false,
|
||||
isEncrypted: true,
|
||||
canonicalAlias: "#alias:domain.com",
|
||||
members: members)
|
||||
let viewModel = RoomDetailsViewModel(roomProxy: roomProxy,
|
||||
mediaProvider: MockMediaProvider())
|
||||
|
||||
@@ -35,7 +35,7 @@ struct RoomMemberDetailsMemberCell: View {
|
||||
.accessibilityHidden(true)
|
||||
|
||||
Text(member.name ?? "")
|
||||
.font(.element.callout.bold())
|
||||
.font(.element.calloutBold)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
.lineLimit(1)
|
||||
|
||||
|
||||
@@ -29,17 +29,18 @@ struct RoomMemberDetailsScreen: View {
|
||||
RoomMemberDetailsMemberCell(member: member, context: context)
|
||||
.id(member.id)
|
||||
}
|
||||
} footer: {
|
||||
} header: {
|
||||
Text(ElementL10n.roomTitleMembers(context.viewState.members.count))
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.font(.element.footnote)
|
||||
.font(.element.body)
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
.searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always))
|
||||
.alert(item: $context.alertInfo) { $0.alert }
|
||||
.navigationTitle(ElementL10n.listMembers)
|
||||
.navigationTitle(ElementL10n.bottomActionPeople)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,15 +31,18 @@ struct SettingsScreen: View {
|
||||
Form {
|
||||
userSection
|
||||
.listRowBackground(rowBackgroundColor)
|
||||
|
||||
analyticsSection
|
||||
|
||||
simplifiedSection
|
||||
.listRowBackground(rowBackgroundColor)
|
||||
|
||||
userInterfaceSection
|
||||
.listRowBackground(rowBackgroundColor)
|
||||
// analyticsSection
|
||||
// .listRowBackground(rowBackgroundColor)
|
||||
|
||||
logoutSection
|
||||
.listRowBackground(rowBackgroundColor)
|
||||
// userInterfaceSection
|
||||
// .listRowBackground(rowBackgroundColor)
|
||||
|
||||
// logoutSection
|
||||
// .listRowBackground(rowBackgroundColor)
|
||||
}
|
||||
.introspectTableView { tableView in
|
||||
tableView.backgroundColor = .clear
|
||||
@@ -59,7 +62,7 @@ struct SettingsScreen: View {
|
||||
}
|
||||
|
||||
private var backgroundColor: Color {
|
||||
.element.systemGray6
|
||||
.element.system
|
||||
}
|
||||
|
||||
private var rowBackgroundColor: Color {
|
||||
@@ -78,14 +81,40 @@ struct SettingsScreen: View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(context.viewState.userDisplayName ?? "")
|
||||
.font(.title3)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
Text(context.viewState.userID)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.element.primaryContent)
|
||||
}
|
||||
}
|
||||
.listRowInsets(listRowInsets)
|
||||
}
|
||||
}
|
||||
|
||||
private var simplifiedSection: some View {
|
||||
Section {
|
||||
SettingsDefaultRow(title: ElementL10n.sendBugReport,
|
||||
image: Image(systemName: "questionmark.circle")) {
|
||||
context.send(viewAction: .reportBug)
|
||||
}
|
||||
.accessibilityIdentifier("reportBugButton")
|
||||
|
||||
SettingsDefaultRow(title: ElementL10n.actionSignOut,
|
||||
image: Image(systemName: "rectangle.portrait.and.arrow.right")) {
|
||||
showingLogoutConfirmation = true
|
||||
}
|
||||
.accessibilityIdentifier("logoutButton")
|
||||
.alert(ElementL10n.actionSignOut,
|
||||
isPresented: $showingLogoutConfirmation) {
|
||||
Button(ElementL10n.actionSignOut,
|
||||
role: .destructive,
|
||||
action: logout)
|
||||
} message: {
|
||||
Text(ElementL10n.actionSignOutConfirmationSimple)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var analyticsSection: some View {
|
||||
Section {
|
||||
Button { context.send(viewAction: .reportBug) } label: {
|
||||
@@ -157,13 +186,8 @@ struct SettingsScreen: View {
|
||||
}
|
||||
|
||||
private var closeButton: some View {
|
||||
Button(action: close) {
|
||||
Image(systemName: "xmark")
|
||||
.font(.title3.bold())
|
||||
.foregroundColor(.element.secondaryContent)
|
||||
.padding(4)
|
||||
}
|
||||
.accessibilityIdentifier("closeButton")
|
||||
Button(ElementL10n.actionCancel, action: close)
|
||||
.accessibilityIdentifier("closeButton")
|
||||
}
|
||||
|
||||
private func close() {
|
||||
|
||||
@@ -274,6 +274,7 @@ class MockScreen: Identifiable {
|
||||
return navigationSplitCoordinator
|
||||
case .roomDetailsScreen:
|
||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||
let userNotificationController = UserNotificationController(rootCoordinator: navigationStackCoordinator)
|
||||
let coordinator = RoomDetailsCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
|
||||
roomProxy: MockRoomProxy(displayName: "Room",
|
||||
isEncrypted: true,
|
||||
|
||||
Reference in New Issue
Block a user