mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
App polish part 3 (#1135)
- Add an animation to long press on messages. - Make emojis bigger on the reactions picker screen. - Remove quote menu action. - Fix emoji picker handle overlapping focused search field. - Update launch, splash and onboarding screens. - Use elevation tokens in Forms. - Move the cancel button on the Start Chat flow.
This commit is contained in:
@@ -491,8 +491,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
}
|
||||
|
||||
private func presentEmojiPicker(for itemId: String) {
|
||||
let emojiPickerNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let params = EmojiPickerScreenCoordinatorParameters(emojiProvider: emojiProvider,
|
||||
itemId: itemId)
|
||||
let coordinator = EmojiPickerScreenCoordinator(parameters: params)
|
||||
@@ -508,11 +506,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
|
||||
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
|
||||
emojiPickerNavigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
emojiPickerNavigationStackCoordinator.presentationDetents = [.medium, .large]
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(emojiPickerNavigationStackCoordinator) { [weak self] in
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(coordinator) { [weak self] in
|
||||
self?.stateMachine.tryEvent(.dismissEmojiPicker)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,18 +30,18 @@ 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 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")
|
||||
internal static let encryptionWarning = ImageAsset(name: "Images/encryption_warning")
|
||||
internal static let launchBackground = ImageAsset(name: "Images/LaunchBackground")
|
||||
internal static let launchLogo = ImageAsset(name: "Images/LaunchLogo")
|
||||
internal static let appLogo = ImageAsset(name: "Images/app-logo")
|
||||
internal static let closeCircle = ImageAsset(name: "Images/close_circle")
|
||||
internal static let timelineComposerSendMessage = ImageAsset(name: "Images/timelineComposerSendMessage")
|
||||
}
|
||||
internal static let launchLogo = ImageAsset(name: "LaunchLogo")
|
||||
internal static let onboardingBackground = ImageAsset(name: "OnboardingBackground")
|
||||
}
|
||||
// swiftlint:enable identifier_name line_length nesting type_body_length type_name
|
||||
|
||||
|
||||
@@ -644,11 +644,13 @@ public enum L10n {
|
||||
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 fastest Element ever. Supercharged for speed and simplicity.
|
||||
public static var screenOnboardingWelcomeMessage: String { return L10n.tr("Localizable", "screen_onboarding_welcome_message") }
|
||||
/// Welcome to %1$@. Supercharged, for speed and simplicity.
|
||||
public static func screenOnboardingWelcomeSubtitle(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_onboarding_welcome_subtitle", String(describing: p1))
|
||||
}
|
||||
/// Be in your Element
|
||||
/// Be in your element
|
||||
public static var screenOnboardingWelcomeTitle: String { return L10n.tr("Localizable", "screen_onboarding_welcome_title") }
|
||||
/// Block user
|
||||
public static var screenReportContentBlockUser: String { return L10n.tr("Localizable", "screen_report_content_block_user") }
|
||||
|
||||
@@ -49,8 +49,8 @@ struct FormRowAccessory: View {
|
||||
switch kind {
|
||||
case .navigationLink:
|
||||
Image(systemName: "chevron.forward")
|
||||
.font(.compound.bodyMD.bold())
|
||||
.foregroundColor(.compound.iconQuaternary)
|
||||
.font(.compound.bodyMDSemibold)
|
||||
.foregroundColor(.compound.iconTertiaryAlpha)
|
||||
case .progressView:
|
||||
ProgressView()
|
||||
case .singleSelection(let isSelected):
|
||||
@@ -111,9 +111,9 @@ struct FormButtonStyle: PrimitiveButtonStyle {
|
||||
private func backgroundColor(for configuration: Configuration) -> Color {
|
||||
switch accessory?.kind {
|
||||
case .none, .navigationLink, .progressView:
|
||||
return configuration.isPressed ? .compound.bgSubtlePrimary : .element.formRowBackground
|
||||
return configuration.isPressed ? .compound.bgSubtlePrimary : .compound.bgCanvasDefaultLevel1
|
||||
default:
|
||||
return .element.formRowBackground
|
||||
return .compound.bgCanvasDefaultLevel1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,7 @@ struct FormActionButtonStyle: ButtonStyle {
|
||||
.frame(width: menuIconSize, height: menuIconSize)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 16)
|
||||
.fill(configuration.isPressed ? Color.compound.bgSubtlePrimary : .element.formRowBackground)
|
||||
.fill(configuration.isPressed ? Color.compound.bgSubtlePrimary : .compound.bgCanvasDefaultLevel1)
|
||||
}
|
||||
|
||||
Text(title)
|
||||
@@ -165,7 +165,7 @@ struct FormButtonStyles_Previews: PreviewProvider {
|
||||
ShareLink(item: "test")
|
||||
.buttonStyle(FormButtonStyle())
|
||||
}
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
|
||||
Section("Single selection") {
|
||||
Button { } label: {
|
||||
@@ -189,7 +189,7 @@ struct FormButtonStyles_Previews: PreviewProvider {
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(accessory: .singleSelection(isSelected: false)))
|
||||
}
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
|
||||
Section("Multiple selection") {
|
||||
Button { } label: {
|
||||
@@ -213,7 +213,7 @@ struct FormButtonStyles_Previews: PreviewProvider {
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(accessory: .multipleSelection(isSelected: false)))
|
||||
}
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
|
||||
Section {
|
||||
Button(role: .destructive) { } label: {
|
||||
@@ -221,7 +221,7 @@ struct FormButtonStyles_Previews: PreviewProvider {
|
||||
}
|
||||
.buttonStyle(FormButtonStyle())
|
||||
}
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ struct FormRowLabelStyle: LabelStyle {
|
||||
|
||||
private var iconBackgroundColor: Color {
|
||||
if role == .destructive {
|
||||
return .compound.textCriticalPrimary.opacity(0.1)
|
||||
return .compound._bgCriticalSubtleAlpha
|
||||
} else {
|
||||
return .element.formBackground
|
||||
return .compound._bgSubtleSecondaryAlpha
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ struct FormRowLabelStyle: LabelStyle {
|
||||
if role == .destructive {
|
||||
return .compound.textCriticalPrimary
|
||||
} else {
|
||||
return .compound.iconSecondary
|
||||
return .compound.iconTertiaryAlpha
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
extension View {
|
||||
/// Applies a standard style to forms.
|
||||
func elementFormStyle() -> some View {
|
||||
scrollContentBackground(.hidden)
|
||||
.background(Color.element.formBackground.ignoresSafeArea())
|
||||
}
|
||||
|
||||
/// Applies a standard style for form header text.
|
||||
func formSectionHeader() -> some View {
|
||||
foregroundColor(.compound.textSecondary)
|
||||
.font(.compound.bodyXS)
|
||||
}
|
||||
|
||||
/// Applies a standard style for form sections.
|
||||
func formSectionStyle() -> some View {
|
||||
listRowSeparator(.hidden)
|
||||
.listRowInsets(FormRow.insets)
|
||||
.listRowBackground(Color.element.formRowBackground)
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ struct CreateRoomScreen: View {
|
||||
topicSection
|
||||
securitySection
|
||||
}
|
||||
.elementFormStyle()
|
||||
.compoundForm()
|
||||
.track(screen: .createRoom)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.navigationTitle(L10n.screenCreateRoomTitle)
|
||||
@@ -83,9 +83,8 @@ struct CreateRoomScreen: View {
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(L10n.screenCreateRoomRoomNameLabel.uppercased())
|
||||
.font(.compound.bodyXS)
|
||||
.padding(.leading, 16)
|
||||
.formSectionHeader()
|
||||
.padding(.leading, FormRow.insets.leading)
|
||||
.compoundFormSectionHeader()
|
||||
TextField(L10n.screenCreateRoomRoomNameLabel,
|
||||
text: $context.roomName,
|
||||
prompt: Text(L10n.commonRoomNamePlaceholder).foregroundColor(.compound.textPlaceholder),
|
||||
@@ -100,7 +99,7 @@ struct CreateRoomScreen: View {
|
||||
.listRowInsets(.init())
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
}
|
||||
|
||||
private var cameraImage: some View {
|
||||
@@ -123,13 +122,13 @@ struct CreateRoomScreen: View {
|
||||
.lineLimit(3, reservesSpace: false)
|
||||
} header: {
|
||||
Text(L10n.screenCreateRoomTopicLabel)
|
||||
.formSectionHeader()
|
||||
.compoundFormSectionHeader()
|
||||
} footer: {
|
||||
if !context.viewState.selectedUsers.isEmpty {
|
||||
selectedUsersSection
|
||||
}
|
||||
}
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
}
|
||||
|
||||
@State private var frame: CGRect = .zero
|
||||
@@ -185,7 +184,7 @@ struct CreateRoomScreen: View {
|
||||
.pickerStyle(.inline)
|
||||
} header: {
|
||||
Text(L10n.commonSecurity.uppercased())
|
||||
.formSectionHeader()
|
||||
.compoundFormSectionHeader()
|
||||
.padding(.top, 40)
|
||||
}
|
||||
.listRowSeparatorTint(.compound.borderDisabled)
|
||||
|
||||
@@ -19,34 +19,44 @@ import SwiftUI
|
||||
|
||||
struct EmojiPickerScreen: View {
|
||||
@ObservedObject var context: EmojiPickerScreenViewModel.Context
|
||||
|
||||
@State var searchString = ""
|
||||
@ScaledMetric(relativeTo: .title) var minimumWidth: Double = 50
|
||||
@State private var isSearching = false
|
||||
|
||||
@ScaledMetric(relativeTo: .title) var minimumWidth: Double = 64
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: minimumWidth))], spacing: 16) {
|
||||
ForEach(context.viewState.categories) { category in
|
||||
Section(header: EmojiPickerScreenHeaderView(title: category.name)
|
||||
.padding(.horizontal, 13)
|
||||
.padding(.top, 10)) {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: minimumWidth))], spacing: 16) {
|
||||
ForEach(context.viewState.categories) { category in
|
||||
Section {
|
||||
ForEach(category.emojis) { emoji in
|
||||
Button {
|
||||
context.send(viewAction: .emojiTapped(emoji: emoji))
|
||||
} label: {
|
||||
Text(emoji.value)
|
||||
.font(.compound.headingLG)
|
||||
.font(.compound.headingXL)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
EmojiPickerScreenHeaderView(title: category.name)
|
||||
.padding(.horizontal, 13)
|
||||
.padding(.top, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 6)
|
||||
}
|
||||
.padding(.horizontal, 6)
|
||||
.navigationTitle(L10n.commonReactions)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbar }
|
||||
.modifier(IsSearching(isSearching: $isSearching))
|
||||
.searchable(text: $searchString, placement: .navigationBarDrawer(displayMode: .always))
|
||||
.compoundSearchField()
|
||||
}
|
||||
.navigationTitle(L10n.commonReactions)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbar }
|
||||
.searchable(text: $searchString, placement: .navigationBarDrawer(displayMode: .always))
|
||||
.compoundSearchField()
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(isSearching ? .hidden : .visible)
|
||||
.onChange(of: searchString) { _ in
|
||||
context.send(viewAction: .search(searchString: searchString))
|
||||
}
|
||||
@@ -62,12 +72,30 @@ struct EmojiPickerScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// A view modifier to extract whether the search field is focussed from a subview.
|
||||
private struct IsSearching: ViewModifier {
|
||||
@Environment(\.isSearching) private var isSearchFieldFocused
|
||||
@Binding var isSearching: Bool
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.onChange(of: isSearchFieldFocused) { isSearching = $0 }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct EmojiPickerScreen_Previews: PreviewProvider {
|
||||
static let viewModel = EmojiPickerScreenViewModel(emojiProvider: EmojiProvider())
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
EmojiPickerScreen(context: EmojiPickerScreenViewModel(emojiProvider: EmojiProvider()).context)
|
||||
}
|
||||
EmojiPickerScreen(context: viewModel.context)
|
||||
.previewDisplayName("Screen")
|
||||
|
||||
Text("Timeline view")
|
||||
.sheet(isPresented: .constant(true)) {
|
||||
EmojiPickerScreen(context: viewModel.context)
|
||||
}
|
||||
.previewDisplayName("Sheet")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ struct InviteUsersScreen: View {
|
||||
|
||||
var body: some View {
|
||||
mainContent
|
||||
.elementFormStyle()
|
||||
.compoundForm()
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.navigationTitle(L10n.screenCreateRoomAddPeopleTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -86,8 +86,7 @@ struct InviteUsersScreen: View {
|
||||
Text(title)
|
||||
}
|
||||
}
|
||||
.listRowSeparator(.automatic)
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
} else {
|
||||
Section.empty
|
||||
}
|
||||
|
||||
@@ -27,10 +27,9 @@ struct MessageForwardingScreen: View {
|
||||
.buttonStyle(FormButtonStyle(accessory: .singleSelection(isSelected: context.viewState.selectedRoomID == room.id)))
|
||||
}
|
||||
}
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
}
|
||||
.listRowSeparator(.automatic)
|
||||
.elementFormStyle()
|
||||
.compoundForm()
|
||||
.navigationTitle(L10n.commonForwardMessage)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
||||
@@ -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
|
||||
|
||||
/// The background gradient shown on the launch, splash and onboarding screens.
|
||||
struct OnboardingBackgroundImage: View {
|
||||
var body: some View {
|
||||
Image(asset: Asset.Images.launchBackground)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.ignoresSafeArea()
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
var isAnimated = true
|
||||
|
||||
@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
|
||||
guard isAnimated else { return }
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -24,57 +24,50 @@ struct OnboardingScreen: View {
|
||||
@ObservedObject var context: OnboardingViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
OnboardingBackgroundView(isAnimated: !Tests.isRunningUITests)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
GeometryReader { geometry in
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
.frame(height: UIConstants.spacerHeight(in: geometry))
|
||||
|
||||
content
|
||||
.frame(width: geometry.size.width)
|
||||
.accessibilityIdentifier(A11yIdentifiers.onboardingScreen.hidden)
|
||||
|
||||
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)
|
||||
GeometryReader { geometry in
|
||||
VStack(alignment: .leading) {
|
||||
Spacer()
|
||||
.frame(height: UIConstants.spacerHeight(in: geometry))
|
||||
|
||||
content
|
||||
.frame(width: geometry.size.width)
|
||||
.accessibilityIdentifier(A11yIdentifiers.onboardingScreen.hidden)
|
||||
|
||||
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))
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.background {
|
||||
OnboardingBackgroundImage()
|
||||
}
|
||||
}
|
||||
|
||||
var content: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
if verticalSizeClass == .regular {
|
||||
Spacer()
|
||||
|
||||
Image(Asset.Images.onboardingAppLogo.name)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.padding(60)
|
||||
Image(asset: Asset.Images.launchLogo)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Spacer()
|
||||
|
||||
Text(L10n.screenOnboardingWelcomeTitle)
|
||||
.font(.compound.headingLGBold)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
Text(L10n.screenOnboardingWelcomeSubtitle(InfoPlistReader.main.bundleDisplayName))
|
||||
Text(L10n.screenOnboardingWelcomeMessage)
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
@@ -18,12 +18,24 @@ import SwiftUI
|
||||
|
||||
class SplashScreenCoordinator: CoordinatorProtocol {
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(
|
||||
ZStack {
|
||||
Image(asset: Asset.Images.onboardingAppLogo)
|
||||
.renderingMode(.original)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
)
|
||||
AnyView(SplashScreen())
|
||||
}
|
||||
}
|
||||
|
||||
/// The app's splash screen. This screen is shown after the LaunchScreen
|
||||
/// until the app is ready to show the relevant coordinator. The design of
|
||||
/// these 2 screens are matched.
|
||||
struct SplashScreen: View {
|
||||
var body: some View {
|
||||
Image(asset: Asset.Images.launchLogo)
|
||||
.accessibilityHidden(true)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background { OnboardingBackgroundImage() }
|
||||
}
|
||||
}
|
||||
|
||||
struct SplashScreen_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SplashScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ struct RoomDetailsEditScreen: View {
|
||||
nameSection
|
||||
topicSection
|
||||
}
|
||||
.elementFormStyle()
|
||||
.compoundForm()
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.navigationTitle(L10n.screenRoomDetailsEditRoomTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -98,9 +98,9 @@ struct RoomDetailsEditScreen: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
} header: {
|
||||
Text(L10n.commonRoomName)
|
||||
.formSectionHeader()
|
||||
.compoundFormSectionHeader()
|
||||
}
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
}
|
||||
|
||||
private var topicSection: some View {
|
||||
@@ -118,9 +118,9 @@ struct RoomDetailsEditScreen: View {
|
||||
.lineLimit(3...)
|
||||
} header: {
|
||||
Text(L10n.commonTopic)
|
||||
.formSectionHeader()
|
||||
.compoundFormSectionHeader()
|
||||
}
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
}
|
||||
|
||||
private var avatarOverlayIcon: some View {
|
||||
|
||||
@@ -43,7 +43,7 @@ struct RoomDetailsScreen: View {
|
||||
|
||||
leaveRoomSection
|
||||
}
|
||||
.elementFormStyle()
|
||||
.compoundForm()
|
||||
.alert(item: $context.alertInfo)
|
||||
.alert(item: $context.leaveRoomAlertItem,
|
||||
actions: leaveRoomAlertActions,
|
||||
@@ -129,9 +129,9 @@ struct RoomDetailsScreen: View {
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.commonTopic)
|
||||
.formSectionHeader()
|
||||
.compoundFormSectionHeader()
|
||||
}
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,9 +159,8 @@ struct RoomDetailsScreen: View {
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.invite)
|
||||
}
|
||||
}
|
||||
.listRowSeparatorTint(.compound.borderDisabled)
|
||||
.buttonStyle(FormButtonStyle(accessory: .navigationLink))
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
}
|
||||
|
||||
@@ -182,9 +181,9 @@ struct RoomDetailsScreen: View {
|
||||
.labelStyle(FormRowLabelStyle(alignment: .top))
|
||||
} header: {
|
||||
Text(L10n.commonSecurity)
|
||||
.formSectionHeader()
|
||||
.compoundFormSectionHeader()
|
||||
}
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +196,7 @@ struct RoomDetailsScreen: View {
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(accessory: nil))
|
||||
}
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -212,7 +211,7 @@ struct RoomDetailsScreen: View {
|
||||
.buttonStyle(FormButtonStyle(accessory: context.viewState.isProcessingIgnoreRequest ? .progressView : nil))
|
||||
.disabled(context.viewState.isProcessingIgnoreRequest)
|
||||
}
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
@@ -27,7 +27,7 @@ struct RoomMemberDetailsScreen: View {
|
||||
blockUserSection
|
||||
}
|
||||
}
|
||||
.elementFormStyle()
|
||||
.compoundForm()
|
||||
.alert(item: $context.ignoreUserAlert, actions: blockUserAlertActions, message: blockUserAlertMessage)
|
||||
.alert(item: $context.alertInfo)
|
||||
.track(screen: .user)
|
||||
@@ -66,7 +66,7 @@ struct RoomMemberDetailsScreen: View {
|
||||
.buttonStyle(FormButtonStyle(accessory: context.viewState.isProcessingIgnoreRequest ? .progressView : nil))
|
||||
.disabled(context.viewState.isProcessingIgnoreRequest)
|
||||
}
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
}
|
||||
|
||||
private var blockUserButtonAction: RoomMemberDetailsScreenViewAction {
|
||||
|
||||
@@ -356,7 +356,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
]
|
||||
|
||||
if timelineItem is EventBasedMessageTimelineItemProtocol {
|
||||
actions.append(contentsOf: [.forward(itemID: itemId), .copy, .quote])
|
||||
actions.append(contentsOf: [.forward(itemID: itemId), .copy])
|
||||
}
|
||||
|
||||
actions.append(.copyPermalink)
|
||||
@@ -403,13 +403,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
state.bindings.composerFocused = true
|
||||
state.bindings.composerText = messageTimelineItem.body
|
||||
setComposerMode(.edit(originalItemId: messageTimelineItem.id))
|
||||
case .quote:
|
||||
guard let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol else {
|
||||
return
|
||||
}
|
||||
|
||||
state.bindings.composerFocused = true
|
||||
state.bindings.composerText = "> \(messageTimelineItem.body)\n\n"
|
||||
case .copyPermalink:
|
||||
do {
|
||||
let permalink = try PermalinkBuilder.permalinkTo(eventIdentifier: eventTimelineItem.id, roomIdentifier: timelineController.roomID)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
struct LongPressWithFeedback: ViewModifier {
|
||||
let action: () -> Void
|
||||
|
||||
@State private var isLongPressing = false
|
||||
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.scaleEffect(x: isLongPressing ? 0.93 : 1,
|
||||
y: isLongPressing ? 0.93 : 1)
|
||||
.animation(isLongPressing ? .spring(response: 0.8).delay(0.2) : .spring(response: 0.2),
|
||||
value: isLongPressing)
|
||||
.onLongPressGesture(minimumDuration: 0.25) {
|
||||
action()
|
||||
feedbackGenerator.impactOccurred()
|
||||
} onPressingChanged: { isPressing in
|
||||
isLongPressing = isPressing
|
||||
if isPressing {
|
||||
feedbackGenerator.prepare()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func longPressWithFeedback(action: @escaping () -> Void) -> some View {
|
||||
modifier(LongPressWithFeedback(action: action))
|
||||
}
|
||||
}
|
||||
|
||||
struct FakeContextMenu_Previews: PreviewProvider {
|
||||
static var previews: some View { Preview() }
|
||||
|
||||
struct Preview: View {
|
||||
private let viewModel = RoomScreenViewModel.mock
|
||||
@State private var isPresentingSheet = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
mockBubble("This is a message from somebody with a couple of lines of text.")
|
||||
.longPressWithFeedback { isPresentingSheet = true }
|
||||
|
||||
mockBubble("Short message")
|
||||
.longPressWithFeedback { isPresentingSheet = true }
|
||||
|
||||
mockBubble("How are you today? The sun is shining here and its very hot ☀️☀️☀️")
|
||||
.longPressWithFeedback { isPresentingSheet = true }
|
||||
|
||||
mockBubble("I'm a fake!")
|
||||
.contextMenu {
|
||||
Button("Copy") { }
|
||||
Button("Reply") { }
|
||||
Button("Remove") { }
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle("Work chat")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
.sheet(isPresented: $isPresentingSheet) {
|
||||
Text("Long pressed!")
|
||||
.presentationDetents([.medium])
|
||||
}
|
||||
.environmentObject(viewModel.context)
|
||||
}
|
||||
|
||||
func mockBubble(_ body: String) -> some View {
|
||||
Text(body)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.compound._bgBubbleOutgoing, in: RoundedRectangle(cornerRadius: 12))
|
||||
.onTapGesture { /* Fix long press gesture blocking the scroll view */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,6 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
||||
@Environment(\.timelineGroupStyle) private var timelineGroupStyle
|
||||
|
||||
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
||||
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
@ViewBuilder let content: () -> Content
|
||||
|
||||
@@ -116,13 +114,8 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
}
|
||||
// We need a tap gesture before this long one so that it doesn't
|
||||
// steal away the gestures from the scroll view
|
||||
.onLongPressGesture(minimumDuration: 0.25) {
|
||||
.longPressWithFeedback {
|
||||
context.send(viewAction: .timelineItemMenu(itemID: timelineItem.id))
|
||||
feedbackGenerator.impactOccurred()
|
||||
} onPressingChanged: { pressing in
|
||||
if pressing {
|
||||
feedbackGenerator.prepare()
|
||||
}
|
||||
}
|
||||
.padding(.top, messageBubbleTopPadding)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ struct TimelineItemMenuActions {
|
||||
enum TimelineItemMenuAction: Identifiable, Hashable {
|
||||
case copy
|
||||
case edit
|
||||
case quote
|
||||
case copyPermalink
|
||||
case redact
|
||||
case reply
|
||||
@@ -182,10 +181,6 @@ public struct TimelineItemMenu: View {
|
||||
Button { send(action) } label: {
|
||||
MenuLabel(title: L10n.actionEdit, systemImageName: "pencil.line")
|
||||
}
|
||||
case .quote:
|
||||
Button { send(action) } label: {
|
||||
MenuLabel(title: L10n.actionQuote, systemImageName: "quote.bubble")
|
||||
}
|
||||
case .copyPermalink:
|
||||
Button { send(action) } label: {
|
||||
MenuLabel(title: L10n.actionCopyLinkToMessage, systemImageName: "link")
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ struct DeveloperOptionsScreen: View {
|
||||
}
|
||||
}
|
||||
.overlay(effectsView)
|
||||
.elementFormStyle()
|
||||
.compoundForm()
|
||||
.navigationTitle(L10n.commonDeveloperOptions)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
@@ -28,13 +28,13 @@ struct StartChatScreen: View {
|
||||
searchContent
|
||||
}
|
||||
}
|
||||
.elementFormStyle()
|
||||
.compoundForm()
|
||||
.track(screen: .startChat)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.navigationTitle(L10n.actionStartChat)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
closeButton
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ struct StartChatScreen: View {
|
||||
}
|
||||
.buttonStyle(FormButtonStyle(accessory: .navigationLink))
|
||||
}
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
}
|
||||
|
||||
private var inviteFriendsSection: some View {
|
||||
@@ -84,7 +84,7 @@ struct StartChatScreen: View {
|
||||
.buttonStyle(FormButtonStyle())
|
||||
.accessibilityIdentifier(A11yIdentifiers.startChatScreen.inviteFriends)
|
||||
}
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -102,8 +102,7 @@ struct StartChatScreen: View {
|
||||
Text(title)
|
||||
}
|
||||
}
|
||||
.listRowSeparator(.automatic)
|
||||
.formSectionStyle()
|
||||
.compoundFormSection()
|
||||
} else {
|
||||
Section.empty
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user