Use Compound ListRow instead of Compound form styles. (#1484)

* Adopt ListRow Component.

* Update snapshots.

To check in future PR:
- Pseudo Bug Report Attach Screenshot label
- Bug Report screenshot padding (Use ListRow .custom?)
- De-bold Analytics & Notification Settings footer links
- Inline picker alignment perhaps?

* Changelog

* Update Compound.

* Use the label on the login screen.
This commit is contained in:
Doug 2023-08-16 16:08:30 +01:00 committed by GitHub
parent 7c69012667
commit c64fb44c96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
98 changed files with 499 additions and 557 deletions

View File

@ -12,7 +12,7 @@ let package = Package(
.library(name: "DesignKit", targets: ["DesignKit"])
],
dependencies: [
.package(url: "https://github.com/vector-im/compound-ios", revision: "89e3ed5adef33be7eb65137b861833ffae64f961"),
.package(url: "https://github.com/vector-im/compound-ios", revision: "50bb7cf313bd1ad17201fc7e4c1184737a0f44c2"),
.package(url: "https://github.com/vector-im/element-design-tokens", exact: "0.0.3"),
.package(url: "https://github.com/siteline/SwiftUI-Introspect", from: "0.9.0")
],

View File

@ -5475,7 +5475,7 @@
repositoryURL = "https://github.com/vector-im/compound-ios";
requirement = {
kind = revision;
revision = 89e3ed5adef33be7eb65137b861833ffae64f961;
revision = 50bb7cf313bd1ad17201fc7e4c1184737a0f44c2;
};
};
9A472EE0218FE7DCF5283429 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {

View File

@ -5,7 +5,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/vector-im/compound-design-tokens.git",
"state" : {
"revision" : "aa55111d94486acbfd3344cf4d08b64723bd6703"
"revision" : "387d2b7211f07761c67e849c59414a1bb803defa"
}
},
{
@ -13,7 +13,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/vector-im/compound-ios",
"state" : {
"revision" : "89e3ed5adef33be7eb65137b861833ffae64f961"
"revision" : "50bb7cf313bd1ad17201fc7e4c1184737a0f44c2"
}
},
{
@ -151,6 +151,15 @@
"version" : "8.6.0"
}
},
{
"identity" : "sfsafesymbols",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols.git",
"state" : {
"revision" : "7cca2d60925876b5953a2cf7341cd80fbeac983c",
"version" : "4.1.1"
}
},
{
"identity" : "swift-algorithms",
"kind" : "remoteSourceControl",

View File

@ -72,29 +72,27 @@ struct LoginScreen: View {
.padding(.horizontal, 16)
.padding(.bottom, 8)
TextField(L10n.commonUsername,
text: $context.username,
// Prompt colour fixes a flicker that occurs before the text field style introspects the field.
prompt: Text(L10n.commonUsername).foregroundColor(.compound.textPlaceholder))
.focused($isUsernameFocused)
.textFieldStyle(.elementInput(accessibilityIdentifier: A11yIdentifiers.loginScreen.emailUsername))
.disableAutocorrection(true)
.textContentType(.username)
.autocapitalization(.none)
.submitLabel(.next)
.onChange(of: isUsernameFocused, perform: usernameFocusChanged)
.onSubmit { isPasswordFocused = true }
.padding(.bottom, 20)
TextField(text: $context.username) {
Text(L10n.commonUsername).foregroundColor(.compound.textPlaceholder)
}
.focused($isUsernameFocused)
.textFieldStyle(.elementInput(accessibilityIdentifier: A11yIdentifiers.loginScreen.emailUsername))
.disableAutocorrection(true)
.textContentType(.username)
.autocapitalization(.none)
.submitLabel(.next)
.onChange(of: isUsernameFocused, perform: usernameFocusChanged)
.onSubmit { isPasswordFocused = true }
.padding(.bottom, 20)
SecureField(L10n.commonPassword,
text: $context.password,
// Prompt colour fixes a flicker that occurs before the text field style introspects the field.
prompt: Text(L10n.commonPassword).foregroundColor(.compound.textPlaceholder))
.focused($isPasswordFocused)
.textFieldStyle(.elementInput(accessibilityIdentifier: A11yIdentifiers.loginScreen.password))
.textContentType(.password)
.submitLabel(.done)
.onSubmit(submit)
SecureField(text: $context.password) {
Text(L10n.commonPassword).foregroundColor(.compound.textPlaceholder)
}
.focused($isPasswordFocused)
.textFieldStyle(.elementInput(accessibilityIdentifier: A11yIdentifiers.loginScreen.password))
.textContentType(.password)
.submitLabel(.done)
.onSubmit(submit)
Spacer().frame(height: 32)

View File

@ -23,6 +23,8 @@ struct BugReportScreen: View {
@ObservedObject var context: BugReportScreenViewModel.Context
var photosPickerTitle: String { context.viewState.screenshot == nil ? L10n.screenBugReportAttachScreenshot : L10n.screenBugReportEditScreenshot }
var body: some View {
Form {
textFieldSection
@ -31,7 +33,7 @@ struct BugReportScreen: View {
canContactSection
}
.scrollDismissesKeyboard(.immediately)
.compoundForm()
.compoundList()
.navigationTitle(L10n.commonReportABug)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
@ -50,53 +52,48 @@ struct BugReportScreen: View {
private var textFieldSection: some View {
Section {
TextField(L10n.screenBugReportEditorPlaceholder,
text: $context.reportText,
prompt: Text(L10n.screenBugReportEditorPlaceholder).compoundFormTextFieldPlaceholder(),
axis: .vertical)
ListRow(label: .plain(title: L10n.screenBugReportEditorPlaceholder),
kind: .textField(text: $context.reportText))
.lineLimit(4, reservesSpace: true)
.textFieldStyle(.compoundForm)
.accessibilityIdentifier(A11yIdentifiers.bugReportScreen.report)
} footer: {
Text(L10n.screenBugReportEditorDescription)
.compoundFormSectionFooter()
.compoundListSectionFooter()
}
.compoundFormSection()
}
private var sendLogsSection: some View {
Section {
Toggle(L10n.screenBugReportIncludeLogs, isOn: $context.sendingLogsEnabled)
.toggleStyle(.compoundForm)
ListRow(label: .plain(title: L10n.screenBugReportIncludeLogs),
kind: .toggle($context.sendingLogsEnabled))
.accessibilityIdentifier(A11yIdentifiers.bugReportScreen.sendLogs)
} footer: {
Text(L10n.screenBugReportLogsDescription)
.compoundFormSectionFooter()
.compoundListSectionFooter()
}
.compoundFormSection()
}
private var canContactSection: some View {
Section {
Toggle(L10n.screenBugReportContactMeTitle, isOn: $context.canContact)
.toggleStyle(.compoundForm)
ListRow(label: .plain(title: L10n.screenBugReportContactMeTitle),
kind: .toggle($context.canContact))
.accessibilityIdentifier(A11yIdentifiers.bugReportScreen.canContact)
} footer: {
Text(L10n.screenBugReportContactMe)
.compoundFormSectionFooter()
.compoundListSectionFooter()
}
.compoundFormSection()
}
@ViewBuilder
private var attachScreenshotSection: some View {
Section {
PhotosPicker(selection: $selectedScreenshot,
matching: .screenshots,
photoLibrary: .shared()) {
Label(context.viewState.screenshot == nil ? L10n.screenBugReportAttachScreenshot : L10n.screenBugReportEditScreenshot, systemImage: "camera")
}
.buttonStyle(.compoundForm())
ListRow(kind: .custom {
PhotosPicker(selection: $selectedScreenshot,
matching: .screenshots,
photoLibrary: .shared()) {
ListLabel.default(title: photosPickerTitle, systemIcon: .camera)
}
})
.accessibilityIdentifier(A11yIdentifiers.bugReportScreen.attachScreenshot)
} footer: {
if let screenshot = context.viewState.screenshot {
@ -117,7 +114,6 @@ struct BugReportScreen: View {
.padding(.horizontal, 16)
}
}
.compoundFormSection()
}
@ToolbarContentBuilder

View File

@ -14,6 +14,7 @@
// limitations under the License.
//
import Compound
import SwiftUI
struct ReportContentScreen: View {
@ -32,7 +33,7 @@ struct ReportContentScreen: View {
ignoreUserSection
}
.scrollDismissesKeyboard(.immediately)
.compoundForm()
.compoundList()
.navigationTitle(L10n.actionReportContent)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
@ -41,27 +42,23 @@ struct ReportContentScreen: View {
private var reasonSection: some View {
Section {
TextField(L10n.reportContentHint,
text: $context.reasonText,
prompt: Text(L10n.reportContentHint).compoundFormTextFieldPlaceholder(),
axis: .vertical)
ListRow(label: .plain(title: L10n.reportContentHint),
kind: .textField(text: $context.reasonText))
.lineLimit(4, reservesSpace: true)
.textFieldStyle(.compoundForm)
} footer: {
Text(L10n.reportContentExplanation)
.compoundFormSectionFooter()
.compoundListSectionFooter()
}
.compoundFormSection()
}
private var ignoreUserSection: some View {
Section {
Toggle(L10n.screenReportContentBlockUser, isOn: $context.ignoreUser)
.toggleStyle(.compoundForm)
ListRow(label: .plain(title: L10n.screenReportContentBlockUser),
kind: .toggle($context.ignoreUser))
.accessibilityIdentifier(A11yIdentifiers.reportContent.ignoreUser)
} footer: {
Text(L10n.screenReportContentBlockUserHint)
.compoundFormSectionFooter()
.compoundListSectionFooter()
}
}

View File

@ -46,6 +46,16 @@ struct RoomDetailsEditScreenViewState: BindableState {
bindings.name != initialName
}
/// The string shown for the room's name when it can't be edited.
var nameRowTitle: String {
bindings.name.isEmpty ? L10n.commonRoomName : bindings.name
}
/// The string shown for the room's topic when it can't be edited.
var topicRowTitle: String {
bindings.topic.isEmpty ? L10n.commonTopic : bindings.topic
}
var topicDidChange: Bool {
bindings.topic != initialTopic
}

View File

@ -14,6 +14,7 @@
// limitations under the License.
//
import Compound
import SwiftUI
struct RoomDetailsEditScreen: View {
@ -31,7 +32,7 @@ struct RoomDetailsEditScreen: View {
nameSection
topicSection
}
.compoundForm()
.compoundList()
.scrollDismissesKeyboard(.immediately)
.navigationTitle(L10n.screenRoomDetailsEditRoomTitle)
.navigationBarTitleDisplayMode(.inline)
@ -85,42 +86,39 @@ struct RoomDetailsEditScreen: View {
private var nameSection: some View {
Section {
let canEditName = context.viewState.canEditName
TextField(L10n.commonRoomName,
text: $context.name,
prompt: canEditName ? Text(L10n.commonRoomNamePlaceholder) : nil,
axis: .horizontal)
.focused($focus, equals: .name)
.textFieldStyle(.compoundForm)
.disabled(!canEditName)
.listRowBackground(canEditName ? Color.element.formRowBackground : .clear)
.clipShape(RoundedRectangle(cornerRadius: 8))
if context.viewState.canEditName {
ListRow(label: .plain(title: L10n.commonRoomNamePlaceholder),
kind: .textField(text: $context.name, axis: .horizontal))
.focused($focus, equals: .name)
} else {
ListRow(kind: .custom {
ListLabel.plain(title: context.viewState.nameRowTitle)
.listRowBackground(Color.clear)
})
}
} header: {
Text(L10n.commonRoomName)
.compoundFormSectionHeader()
.compoundListSectionHeader()
}
.compoundFormSection()
}
private var topicSection: some View {
Section {
let canEditTopic = context.viewState.canEditTopic
TextField(L10n.commonTopic,
text: $context.topic,
prompt: canEditTopic ? Text(L10n.commonTopicPlaceholder).foregroundColor(.compound.textPlaceholder) : nil,
axis: .vertical)
.focused($focus, equals: .topic)
.textFieldStyle(.compoundForm)
.disabled(!canEditTopic)
.listRowBackground(canEditTopic ? Color.element.formRowBackground : .clear)
.lineLimit(3...)
if context.viewState.canEditTopic {
ListRow(label: .plain(title: L10n.commonTopicPlaceholder),
kind: .textField(text: $context.topic, axis: .vertical))
.focused($focus, equals: .topic)
.lineLimit(3...)
} else {
ListRow(kind: .custom {
ListLabel.plain(title: context.viewState.topicRowTitle)
.listRowBackground(Color.clear)
})
}
} header: {
Text(L10n.commonTopic)
.compoundFormSectionHeader()
.compoundListSectionHeader()
}
.compoundFormSection()
}
private var avatarOverlayIcon: some View {
@ -166,9 +164,23 @@ struct RoomDetailsEditScreen_Previews: PreviewProvider {
roomProxy: RoomProxyMock(with: .init(name: "Room", displayName: "Room")),
userIndicatorController: UserIndicatorControllerMock.default)
static let readOnlyViewModel = {
let accountOwner = RoomMemberProxyMock.mockOwner(allowedStateEvents: [])
return RoomDetailsEditScreenViewModel(accountOwner: accountOwner,
mediaProvider: MockMediaProvider(),
roomProxy: RoomProxyMock(with: .init(name: "Room", displayName: "Room")),
userIndicatorController: UserIndicatorControllerMock.default)
}()
static var previews: some View {
NavigationStack {
RoomDetailsEditScreen(context: viewModel.context)
}
.previewDisplayName("Normal")
NavigationStack {
RoomDetailsEditScreen(context: readOnlyViewModel.context)
}
.previewDisplayName("Read only")
}
}

View File

@ -14,6 +14,7 @@
// limitations under the License.
//
import Compound
import SwiftUI
struct RoomDetailsScreen: View {
@ -47,7 +48,7 @@ struct RoomDetailsScreen: View {
leaveRoomSection
}
.compoundForm()
.compoundList()
.alert(item: $context.alertInfo)
.alert(item: $context.leaveRoomAlertItem,
actions: leaveRoomAlertActions,
@ -125,74 +126,55 @@ struct RoomDetailsScreen: View {
if context.viewState.hasTopicSection {
Section {
if let topic = context.viewState.topic, !topic.isEmpty {
Text(topic)
.compoundFormSecondaryTextRow()
ListRow(label: .description(topic), kind: .label)
.lineLimit(isTopicExpanded ? nil : 3)
.onTapGesture { isTopicExpanded.toggle() }
} else {
Button {
context.send(viewAction: .processTapAddTopic)
} label: {
Text(L10n.screenRoomDetailsAddTopicTitle)
}
.buttonStyle(.compoundForm())
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.addTopic)
ListRow(label: .plain(title: L10n.screenRoomDetailsAddTopicTitle),
kind: .button { context.send(viewAction: .processTapAddTopic) })
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.addTopic)
}
} header: {
Text(L10n.commonTopic)
.compoundFormSectionHeader()
.compoundListSectionHeader()
}
.compoundFormSection()
}
}
private var aboutSection: some View {
Section {
Button {
context.send(viewAction: .processTapPeople)
} label: {
LabeledContent {
Text(String(context.viewState.joinedMembersCount))
} label: {
Label(L10n.commonPeople, systemImage: "person")
}
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people)
ListRow(label: .default(title: L10n.commonPeople, systemIcon: .person),
details: .title(String(context.viewState.joinedMembersCount)),
kind: .navigationLink {
context.send(viewAction: .processTapPeople)
})
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people)
if context.viewState.canInviteUsers {
Button {
context.send(viewAction: .processTapInvite)
} label: {
Label(L10n.screenRoomDetailsInvitePeopleTitle, systemImage: "person.badge.plus")
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.invite)
ListRow(label: .default(title: L10n.screenRoomDetailsInvitePeopleTitle,
systemIcon: .personBadgePlus),
kind: .navigationLink {
context.send(viewAction: .processTapInvite)
})
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.invite)
}
}
.buttonStyle(.compoundForm(accessory: .navigationLink))
.compoundFormSection()
}
@ViewBuilder
private var notificationSection: some View {
Section {
Button {
context.send(viewAction: .processTapNotifications)
} label: {
LabeledContent {
if context.viewState.notificationSettingsState.isLoading {
ProgressView()
} else if context.viewState.notificationSettingsState.isError {
Image(systemName: "exclamationmark.circle")
} else {
Text(context.viewState.notificationSettingsState.label)
}
} label: {
Label(L10n.screenRoomDetailsNotificationTitle, systemImage: "bell")
}
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.notifications)
ListRow(label: .default(title: L10n.screenRoomDetailsNotificationTitle,
systemIcon: .bell),
details: context.viewState.notificationSettingsState.isLoading ? .isWaiting(true)
: context.viewState.notificationSettingsState.isError ? .systemIcon(.exclamationmarkCircle)
: .title(context.viewState.notificationSettingsState.label),
kind: .navigationLink {
context.send(viewAction: .processTapNotifications)
})
.disabled(context.viewState.notificationSettingsState.isLoading)
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.notifications)
}
.buttonStyle(.compoundForm(accessory: context.viewState.notificationSettingsState.isLoaded ? .navigationLink : nil))
.disabled(context.viewState.notificationSettingsState.isLoading)
}
@ -215,51 +197,38 @@ struct RoomDetailsScreen: View {
private var securitySection: some View {
if context.viewState.isEncrypted {
Section {
Label {
Text(L10n.screenRoomDetailsEncryptionEnabledTitle)
} icon: {
Image(systemName: "lock.shield")
}
.labelStyle(.compoundFormRow(secondaryText: L10n.screenRoomDetailsEncryptionEnabledSubtitle,
alignment: .top))
ListRow(label: .default(title: L10n.screenRoomDetailsEncryptionEnabledTitle,
description: L10n.screenRoomDetailsEncryptionEnabledSubtitle,
systemIcon: .lockShield,
iconAlignment: .top),
kind: .label)
} header: {
Text(L10n.commonSecurity)
.compoundFormSectionHeader()
.compoundListSectionHeader()
}
.compoundFormSection()
}
}
private var leaveRoomSection: some View {
Section {
Button(role: .destructive) {
context.send(viewAction: .processTapLeave)
} label: {
Label(L10n.actionLeaveRoom, systemImage: "door.right.hand.open")
}
.buttonStyle(.compoundForm())
ListRow(label: .default(title: L10n.actionLeaveRoom,
systemIcon: .doorRightHandOpen,
role: .destructive),
kind: .button { context.send(viewAction: .processTapLeave) })
}
.compoundFormSection()
}
private func ignoreUserSection(user: RoomMemberDetails) -> some View {
Section {
Button(role: user.isIgnored ? nil : .destructive) {
context.send(viewAction: user.isIgnored ? .processTapUnignore : .processTapIgnore)
} label: {
LabeledContent {
if context.viewState.isProcessingIgnoreRequest {
ProgressView()
}
} label: {
Label(user.isIgnored ? L10n.screenDmDetailsUnblockUser : L10n.screenDmDetailsBlockUser,
systemImage: "slash.circle")
}
}
.buttonStyle(.compoundForm())
.disabled(context.viewState.isProcessingIgnoreRequest)
ListRow(label: .default(title: user.isIgnored ? L10n.screenDmDetailsUnblockUser : L10n.screenDmDetailsBlockUser,
systemIcon: .slashCircle,
role: user.isIgnored ? nil : .destructive),
details: .isWaiting(context.viewState.isProcessingIgnoreRequest),
kind: .button {
context.send(viewAction: user.isIgnored ? .processTapUnignore : .processTapIgnore)
})
.disabled(context.viewState.isProcessingIgnoreRequest)
}
.compoundFormSection()
}
@ViewBuilder

View File

@ -54,7 +54,7 @@ struct RoomNotificationSettingsScreenViewState: BindableState {
let strings = RoomNotificationSettingsScreenStrings()
var notificationSettingsState: RoomNotificationSettingsState = .loading
var availableCustomRoomNotificationModes: [RoomNotificationModeProxy] = [.allMessages, .mentionsAndKeywordsOnly, .mute]
var isRestoringDefautSetting = false
var isRestoringDefaultSetting = false
var pendingCustomMode: RoomNotificationModeProxy?
}

View File

@ -82,7 +82,7 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie
isOneToOne: roomProxy.activeMembersCount == 2)
guard !Task.isCancelled else { return }
state.notificationSettingsState = .loaded(settings: settings)
if !state.isRestoringDefautSetting {
if !state.isRestoringDefaultSetting {
state.bindings.allowCustomSetting = !settings.isDefault
}
if state.pendingCustomMode == nil {
@ -106,14 +106,14 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie
}
private func restoreDefaultSetting() {
state.isRestoringDefautSetting = true
state.isRestoringDefaultSetting = true
Task {
do {
try await notificationSettingsProxy.restoreDefaultNotificationMode(roomId: roomProxy.id)
} catch {
displayError(.restoreDefaultFailed)
}
state.isRestoringDefautSetting = false
state.isRestoringDefaultSetting = false
}
}

View File

@ -14,6 +14,7 @@
// limitations under the License.
//
import Compound
import SwiftUI
struct RoomNotificationSettingsScreen: View {
@ -29,7 +30,7 @@ struct RoomNotificationSettingsScreen: View {
customSettingsSection
}
}
.compoundForm()
.compoundList()
.navigationTitle(L10n.screenRoomDetailsNotificationTitle)
.alert(item: $context.alertInfo)
.track(screen: .roomNotifications)
@ -40,35 +41,28 @@ struct RoomNotificationSettingsScreen: View {
@ViewBuilder
private var allowCustomSettingSection: some View {
Section {
Toggle(isOn: $context.allowCustomSetting) {
Text(L10n.screenRoomNotificationSettingsAllowCustom)
}
.toggleStyle(.compoundForm)
.accessibilityIdentifier(A11yIdentifiers.roomNotificationSettingsScreen.allowCustomSetting)
.disabled(context.viewState.notificationSettingsState.isLoading)
.onChange(of: context.allowCustomSetting) { _ in
context.send(viewAction: .changedAllowCustomSettings)
}
ListRow(label: .plain(title: L10n.screenRoomNotificationSettingsAllowCustom),
kind: .toggle($context.allowCustomSetting))
.accessibilityIdentifier(A11yIdentifiers.roomNotificationSettingsScreen.allowCustomSetting)
.disabled(context.viewState.notificationSettingsState.isLoading)
.onChange(of: context.allowCustomSetting) { _ in
context.send(viewAction: .changedAllowCustomSettings)
}
} footer: {
Text(L10n.screenRoomNotificationSettingsAllowCustomFootnote)
.compoundFormSectionFooter()
.compoundListSectionFooter()
}
.compoundFormSection()
}
@ViewBuilder
private var defaultSettingSection: some View {
Section {
if context.viewState.isRestoringDefautSetting {
Text(L10n.commonLoading)
.foregroundColor(.compound.textPlaceholder)
} else {
Text(context.viewState.strings.string(for: context.viewState.notificationSettingsState))
.foregroundColor(.compound.textPrimary)
}
ListRow(label: .plain(title: context.viewState.isRestoringDefaultSetting ? L10n.commonLoading : context.viewState.strings.string(for: context.viewState.notificationSettingsState)),
kind: .label)
.disabled(context.viewState.isRestoringDefaultSetting)
} header: {
Text(L10n.screenRoomNotificationSettingsDefaultSettingTitle)
.compoundFormSectionHeader()
.compoundListSectionHeader()
} footer: {
Text(context.viewState.strings.customSettingFootnote)
.environment(\.openURL, OpenURLAction { url in
@ -76,30 +70,25 @@ struct RoomNotificationSettingsScreen: View {
context.send(viewAction: .customSettingFootnoteLinkTapped)
return .handled
})
.compoundFormSectionFooter()
.compoundListSectionFooter()
}
.compoundFormSection()
}
@ViewBuilder
private var customSettingsSection: some View {
Section {
Picker("", selection: $context.customMode) {
ForEach(context.viewState.availableCustomRoomNotificationModes, id: \.self) { mode in
Text(context.viewState.strings.string(for: mode))
.tag(mode)
}
}
.onChange(of: context.customMode) { mode in
context.send(viewAction: .setCustomMode(mode))
}
.labelsHidden()
.pickerStyle(.inline)
ListRow(label: .plain(title: L10n.screenRoomNotificationSettingsCustomSettingsTitle),
kind: .inlinePicker(selection: $context.customMode,
items: context.viewState.availableCustomRoomNotificationModes.map {
(title: context.viewState.strings.string(for: $0), tag: $0)
}))
.onChange(of: context.customMode) { mode in
context.send(viewAction: .setCustomMode(mode))
}
} header: {
Text(L10n.screenRoomNotificationSettingsCustomSettingsTitle)
.compoundFormSectionHeader()
.compoundListSectionHeader()
}
.compoundFormSection()
}
}

View File

@ -14,6 +14,7 @@
// limitations under the License.
//
import Compound
import SwiftUI
struct AnalyticsSettingsScreen: View {
@ -23,25 +24,23 @@ struct AnalyticsSettingsScreen: View {
Form {
analyticsSection
}
.compoundForm()
.compoundList()
.navigationTitle(L10n.commonAnalytics)
.navigationBarTitleDisplayMode(.inline)
}
var analyticsSection: some View {
Section {
Toggle(isOn: $context.enableAnalytics) {
Label(L10n.screenAnalyticsSettingsShareData, systemImage: "chart.bar")
}
.toggleStyle(.compoundForm)
.onChange(of: context.enableAnalytics) { _ in
context.send(viewAction: .toggleAnalytics)
}
ListRow(label: .default(title: L10n.screenAnalyticsSettingsShareData,
systemIcon: .chartBar),
kind: .toggle($context.enableAnalytics))
.onChange(of: context.enableAnalytics) { _ in
context.send(viewAction: .toggleAnalytics)
}
} footer: {
Text(context.viewState.strings.sectionFooter)
.compoundFormSectionFooter()
.compoundListSectionFooter()
}
.compoundFormSection()
}
}

View File

@ -14,6 +14,7 @@
// limitations under the License.
//
import Compound
import SwiftUI
struct LegalInformationScreen: View {
@ -23,20 +24,15 @@ struct LegalInformationScreen: View {
var body: some View {
Form {
Section {
Button(L10n.commonCopyright) {
openURL("https://element.io/copyright")
}
Button(L10n.commonAcceptableUsePolicy) {
openURL("https://element.io/acceptable-use-policy-terms")
}
Button(L10n.commonPrivacyPolicy) {
openURL("https://element.io/privacy")
}
ListRow(label: .plain(title: L10n.commonCopyright),
kind: .navigationLink { openURL("https://element.io/copyright") })
ListRow(label: .plain(title: L10n.commonAcceptableUsePolicy),
kind: .navigationLink { openURL("https://element.io/acceptable-use-policy-terms") })
ListRow(label: .plain(title: L10n.commonPrivacyPolicy),
kind: .navigationLink { openURL("https://element.io/privacy") })
}
.buttonStyle(.compoundForm(accessory: .navigationLink))
.compoundFormSection()
}
.compoundForm()
.compoundList()
.navigationTitle(L10n.commonAbout)
.navigationBarTitleDisplayMode(.inline)
}

View File

@ -14,6 +14,7 @@
// limitations under the License.
//
import Compound
import SwiftUI
struct NotificationSettingsScreen: View {
@ -35,7 +36,7 @@ struct NotificationSettingsScreen: View {
}
}
}
.compoundForm()
.compoundList()
.navigationTitle(L10n.screenNotificationSettingsTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
@ -58,120 +59,98 @@ struct NotificationSettingsScreen: View {
private var userPermissionSection: some View {
Section {
HStack(alignment: .firstTextBaseline, spacing: 13) {
Image(systemName: "exclamationmark.circle.fill")
.foregroundColor(.compound.iconTertiaryAlpha)
VStack(alignment: .leading, spacing: 2) {
Text(L10n.screenNotificationSettingsSystemNotificationsTurnedOff)
.font(.compound.bodyLG)
.foregroundColor(.compound.textPrimary)
Text(context.viewState.strings.changeYourSystemSettings)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
.tint(.compound.textPrimary)
.environment(\.openURL, OpenURLAction { url in
context.send(viewAction: .linkClicked(url: url))
return .systemAction
})
ListRow(kind: .custom {
HStack(alignment: .firstTextBaseline, spacing: 13) {
Image(systemSymbol: .exclamationmarkCircleFill)
.foregroundColor(.compound.iconTertiaryAlpha)
VStack(alignment: .leading, spacing: 2) {
Text(L10n.screenNotificationSettingsSystemNotificationsTurnedOff)
.font(.compound.bodyLG)
.foregroundColor(.compound.textPrimary)
Text(context.viewState.strings.changeYourSystemSettings)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
.tint(.compound.textPrimary)
}
}
.padding(.vertical, 5)
}
.padding(.horizontal, ListRowPadding.horizontal)
.padding(.vertical, 8)
.environment(\.openURL, OpenURLAction { url in
context.send(viewAction: .linkClicked(url: url))
return .systemAction
})
})
}
.compoundFormSection()
}
private var enableNotificationSection: some View {
Section {
Toggle(isOn: $context.enableNotifications) {
Text(L10n.screenNotificationSettingsEnableNotifications)
}
.toggleStyle(.compoundForm)
.onChange(of: context.enableNotifications) { _ in
context.send(viewAction: .changedEnableNotifications)
}
ListRow(label: .plain(title: L10n.screenNotificationSettingsEnableNotifications),
kind: .toggle($context.enableNotifications))
.onChange(of: context.enableNotifications) { _ in
context.send(viewAction: .changedEnableNotifications)
}
}
.compoundFormSection()
}
private var roomsNotificationSection: some View {
Section {
// Group chats
Button {
context.send(viewAction: .groupChatsTapped)
} label: {
LabeledContent {
if let settings = context.viewState.settings {
Text(context.viewState.strings.string(for: settings.groupChatsMode))
} else {
ProgressView()
}
} label: {
Text(L10n.screenNotificationSettingsGroupChats)
}
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.notifications)
.buttonStyle(.compoundForm(accessory: context.viewState.settings.flatMap { _ in .navigationLink }))
.disabled(context.viewState.settings == nil)
ListRow(label: .plain(title: L10n.screenNotificationSettingsGroupChats),
details: context.viewState.settings.map {
ListDetailsLabel.title(context.viewState.strings.string(for: $0.groupChatsMode))
} ?? .isWaiting(true),
kind: .navigationLink {
context.send(viewAction: .groupChatsTapped)
})
.disabled(context.viewState.settings == nil)
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.notifications)
// Direct chats
Button {
context.send(viewAction: .directChatsTapped)
} label: {
LabeledContent {
if let settings = context.viewState.settings {
Text(context.viewState.strings.string(for: settings.directChatsMode))
} else {
ProgressView()
}
} label: {
Text(L10n.screenNotificationSettingsDirectChats)
}
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.notifications)
.buttonStyle(.compoundForm(accessory: context.viewState.settings.flatMap { _ in .navigationLink }))
.disabled(context.viewState.settings == nil)
ListRow(label: .plain(title: L10n.screenNotificationSettingsDirectChats),
details: context.viewState.settings.map {
ListDetailsLabel.title(context.viewState.strings.string(for: $0.directChatsMode))
} ?? .isWaiting(true),
kind: .navigationLink {
context.send(viewAction: .directChatsTapped)
})
.disabled(context.viewState.settings == nil)
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.notifications)
} header: {
Text(L10n.screenNotificationSettingsNotificationSectionTitle)
.compoundFormSectionHeader()
.compoundListSectionHeader()
}
.compoundFormSection()
}
private var mentionsSection: some View {
Section {
Toggle(isOn: $context.roomMentionsEnabled) {
Text(L10n.screenNotificationSettingsRoomMentionLabel)
}
.toggleStyle(.compoundForm)
.onChange(of: context.roomMentionsEnabled) { _ in
context.send(viewAction: .roomMentionChanged)
}
.disabled(context.viewState.settings?.roomMentionsEnabled == nil)
.allowsHitTesting(!context.viewState.applyingChange)
ListRow(label: .plain(title: L10n.screenNotificationSettingsRoomMentionLabel),
kind: .toggle($context.roomMentionsEnabled))
.disabled(context.viewState.settings?.roomMentionsEnabled == nil)
.allowsHitTesting(!context.viewState.applyingChange)
.onChange(of: context.roomMentionsEnabled) { _ in
context.send(viewAction: .roomMentionChanged)
}
} header: {
Text(L10n.screenNotificationSettingsMentionsSectionTitle)
.compoundFormSectionHeader()
.compoundListSectionHeader()
}
.compoundFormSection()
}
private var callsSection: some View {
Section {
Toggle(isOn: $context.callsEnabled) {
Text(L10n.screenNotificationSettingsCallsLabel)
}
.toggleStyle(.compoundForm)
.onChange(of: context.callsEnabled) { _ in
context.send(viewAction: .callsChanged)
}
.disabled(context.viewState.settings?.callsEnabled == nil)
.allowsHitTesting(!context.viewState.applyingChange)
ListRow(label: .plain(title: L10n.screenNotificationSettingsCallsLabel),
kind: .toggle($context.callsEnabled))
.disabled(context.viewState.settings?.callsEnabled == nil)
.allowsHitTesting(!context.viewState.applyingChange)
.onChange(of: context.callsEnabled) { _ in
context.send(viewAction: .callsChanged)
}
} header: {
Text(L10n.screenNotificationSettingsAdditionalSettingsSectionTitle)
.compoundFormSectionHeader()
.compoundListSectionHeader()
}
.compoundFormSection()
}
}

View File

@ -38,7 +38,7 @@ struct SettingsScreen: View {
signOutSection
}
.compoundForm()
.compoundList()
.navigationTitle(L10n.commonSettings)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
@ -48,118 +48,118 @@ struct SettingsScreen: View {
}
}
private var versionText: some View {
private var versionText: Text {
Text(L10n.settingsVersionNumber(InfoPlistReader.main.bundleShortVersionString, InfoPlistReader.main.bundleVersion))
}
private var userSection: some View {
Section {
HStack(spacing: 12) {
LoadableAvatarImage(url: context.viewState.userAvatarURL,
name: context.viewState.userDisplayName,
contentID: context.viewState.userID,
avatarSize: .user(on: .settings),
imageProvider: context.imageProvider)
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 2) {
Text(context.viewState.userDisplayName ?? "")
.font(.compound.headingMD)
.foregroundColor(.compound.textPrimary)
Text(context.viewState.userID)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
ListRow(kind: .custom {
HStack(spacing: 12) {
LoadableAvatarImage(url: context.viewState.userAvatarURL,
name: context.viewState.userDisplayName,
contentID: context.viewState.userID,
avatarSize: .user(on: .settings),
imageProvider: context.imageProvider)
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 2) {
Text(context.viewState.userDisplayName ?? "")
.font(.compound.headingMD)
.foregroundColor(.compound.textPrimary)
Text(context.viewState.userID)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
}
.accessibilityElement(children: .combine)
}
.accessibilityElement(children: .combine)
}
.padding(.horizontal, ListRowPadding.horizontal)
.padding(.vertical, 8)
})
}
.compoundFormSection()
}
private var sessionVerificationSection: some View {
Section {
Button { context.send(viewAction: .sessionVerification) } label: {
Label(L10n.actionCompleteVerification, systemImage: "checkmark.shield")
}
.buttonStyle(.compoundForm())
ListRow(label: .default(title: L10n.actionCompleteVerification,
systemIcon: .checkmarkShield),
kind: .button { context.send(viewAction: .sessionVerification) })
}
.compoundFormSection()
}
private var developerOptionsSection: some View {
Section {
Button { context.send(viewAction: .developerOptions) } label: {
Label(L10n.commonDeveloperOptions, systemImage: "hammer.circle")
}
.buttonStyle(.compoundForm(accessory: .navigationLink))
.accessibilityIdentifier("developerOptionsButton")
ListRow(label: .default(title: L10n.commonDeveloperOptions,
systemIcon: .hammerCircle),
kind: .navigationLink {
context.send(viewAction: .developerOptions)
})
.accessibilityIdentifier("developerOptionsButton")
}
.compoundFormSection()
}
private var simplifiedSection: some View {
Section {
Picker(selection: $context.timelineStyle) {
ForEach(TimelineStyle.allCases, id: \.self) { style in
Text(style.name)
.tag(style)
ListRow(label: .default(title: L10n.commonMessageLayout,
systemIcon: .rectangleGrid1x2),
kind: .picker(selection: $context.timelineStyle,
items: TimelineStyle.allCases.map { (title: $0.name, tag: $0) }))
.accessibilityIdentifier("timelineStylePicker")
.onChange(of: context.timelineStyle) { _ in
context.send(viewAction: .changedTimelineStyle)
}
} label: {
Label(L10n.commonMessageLayout, systemImage: "rectangle.grid.1x2")
}
.labelStyle(.compoundFormRow())
.accessibilityIdentifier("timelineStylePicker")
.onChange(of: context.timelineStyle) { _ in
context.send(viewAction: .changedTimelineStyle)
}
// Notifications
if context.viewState.showNotificationSettings {
Button { context.send(viewAction: .notifications) } label: {
Label(L10n.screenNotificationSettingsTitle, systemImage: "bell")
}
.buttonStyle(.compoundForm(accessory: .navigationLink))
.accessibilityIdentifier("notificationsButton")
ListRow(label: .default(title: L10n.screenNotificationSettingsTitle,
systemIcon: .bell),
kind: .navigationLink {
context.send(viewAction: .notifications)
})
.accessibilityIdentifier("notificationsButton")
}
// Analytics
Button { context.send(viewAction: .analytics) } label: {
Label(L10n.commonAnalytics, systemImage: "chart.bar")
}
.buttonStyle(.compoundForm(accessory: .navigationLink))
.accessibilityIdentifier("analyticsButton")
ListRow(label: .default(title: L10n.commonAnalytics,
systemIcon: .chartBar),
kind: .navigationLink {
context.send(viewAction: .analytics)
})
.accessibilityIdentifier("analyticsButton")
// Report Bug
Button { context.send(viewAction: .reportBug) } label: {
Label(L10n.commonReportABug, systemImage: "ladybug")
}
.buttonStyle(.compoundForm(accessory: .navigationLink))
.accessibilityIdentifier("reportBugButton")
ListRow(label: .default(title: L10n.commonReportABug,
systemIcon: .ladybug),
kind: .navigationLink {
context.send(viewAction: .reportBug)
})
.accessibilityIdentifier("reportBugButton")
// About
Button { context.send(viewAction: .about) } label: {
Label(L10n.commonAbout, systemImage: "questionmark.circle")
}
.buttonStyle(.compoundForm(accessory: .navigationLink))
.accessibilityIdentifier("aboutButton")
ListRow(label: .default(title: L10n.commonAbout,
systemIcon: .questionmarkCircle),
kind: .navigationLink {
context.send(viewAction: .about)
})
.accessibilityIdentifier("aboutButton")
}
.compoundFormSection()
}
private var signOutSection: some View {
Section {
Button { showingLogoutConfirmation = true } label: {
Label(L10n.screenSignoutPreferenceItem, systemImage: "rectangle.portrait.and.arrow.right")
}
.buttonStyle(.compoundForm())
.accessibilityIdentifier("logoutButton")
.alert(L10n.screenSignoutConfirmationDialogTitle, isPresented: $showingLogoutConfirmation) {
Button(L10n.screenSignoutConfirmationDialogSubmit,
role: .destructive,
action: logout)
} message: {
Text(L10n.screenSignoutConfirmationDialogContent)
}
ListRow(label: .default(title: L10n.screenSignoutPreferenceItem,
systemIcon: .rectanglePortraitAndArrowRight),
kind: .button {
showingLogoutConfirmation = true
})
.accessibilityIdentifier("logoutButton")
.alert(L10n.screenSignoutConfirmationDialogTitle, isPresented: $showingLogoutConfirmation) {
Button(L10n.screenSignoutConfirmationDialogSubmit,
role: .destructive,
action: logout)
} message: {
Text(L10n.screenSignoutConfirmationDialogContent)
}
} footer: {
VStack {
versionText
@ -169,11 +169,10 @@ struct SettingsScreen: View {
Text(deviceID)
}
}
.compoundFormSectionFooter()
.compoundListSectionFooter()
.textSelection(.enabled)
.padding(.top, 24)
}
.compoundFormSection()
}
private var doneButton: some View {
@ -204,7 +203,10 @@ private extension TimelineStyle {
struct SettingsScreen_Previews: PreviewProvider {
static let viewModel = {
let userSession = MockUserSession(clientProxy: MockClientProxy(userID: "@userid:example.com",
let verificationController = SessionVerificationControllerProxyMock()
verificationController.isVerified = false
let userSession = MockUserSession(sessionVerificationController: verificationController,
clientProxy: MockClientProxy(userID: "@userid:example.com",
deviceID: "AAAAAAAAAAA"),
mediaProvider: MockMediaProvider())
ServiceLocator.shared.settings.notificationSettingsEnabled = true

View File

@ -18,7 +18,7 @@ import Combine
struct MockUserSession: UserSessionProtocol {
let callbacks = PassthroughSubject<UserSessionCallback, Never>()
let sessionVerificationController: SessionVerificationControllerProxyProtocol? = nil
var sessionVerificationController: SessionVerificationControllerProxyProtocol?
var userID: String { clientProxy.userID }
var deviceID: String? { clientProxy.deviceID }
var homeserver: String { clientProxy.homeserver }

View File

@ -149,7 +149,10 @@ class MockScreen: Identifiable {
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .templateScreen:
return TemplateScreenCoordinator(parameters: .init())
let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = TemplateScreenCoordinator(parameters: .init())
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .home:
let navigationStackCoordinator = NavigationStackCoordinator()
let session = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:matrix.org"),

View File

@ -14,6 +14,7 @@
// limitations under the License.
//
import Compound
import SwiftUI
struct TemplateScreen: View {
@ -22,22 +23,15 @@ struct TemplateScreen: View {
var body: some View {
Form {
Section {
TextField(text: $context.composerText) {
Text(context.viewState.placeholder)
.compoundFormTextFieldPlaceholder()
}
.textFieldStyle(.compoundForm)
ListRow(label: .plain(title: context.viewState.placeholder),
kind: .textField(text: $context.composerText))
Button {
context.send(viewAction: .done)
} label: {
Label("Done", systemImage: "door.left.hand.closed")
}
.buttonStyle(.compoundFormCentred())
ListRow(label: .centeredAction(title: L10n.actionDone,
systemIcon: .doorLeftHandClosed),
kind: .button { context.send(viewAction: .done) })
}
.compoundFormSection()
}
.compoundForm()
.compoundList()
.navigationTitle(context.viewState.title)
.onChange(of: context.composerText) { _ in
context.send(viewAction: .textChanged)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -83,7 +83,7 @@ class RoomNotificationSettingsScreenViewModelTests: XCTestCase {
notificationSettingsProxyMock.callbacks.send(.settingsDidChange)
try await deferred.fulfill()
let deferredIsRestoringDefaultSettings = deferFulfillment(context.$viewState.map(\.isRestoringDefautSetting)
let deferredIsRestoringDefaultSettings = deferFulfillment(context.$viewState.map(\.isRestoringDefaultSetting)
.removeDuplicates()
.collect(3).first())
viewModel.state.bindings.allowCustomSetting = false

View File

@ -0,0 +1 @@
Use Compound ListRow instead of .xyzStyle(.compoundRow)

View File

@ -51,7 +51,7 @@ packages:
path: DesignKit
Compound:
url: https://github.com/vector-im/compound-ios
revision: 89e3ed5adef33be7eb65137b861833ffae64f961
revision: 50bb7cf313bd1ad17201fc7e4c1184737a0f44c2
# path: ../compound-ios
AnalyticsEvents:
url: https://github.com/matrix-org/matrix-analytics-events