Extract and start to split theme.

This commit is contained in:
David Langley
2021-08-27 16:26:56 +01:00
parent 199709978a
commit cb0403ed8d
67 changed files with 349 additions and 173 deletions

View File

@@ -16,53 +16,60 @@
import Foundation
import UIKit
import SwiftUI
public protocol DesignKitColorType { }
extension UIColor: DesignKitColorType { }
extension Color : DesignKitColorType { }
/// Colors at https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=1255%3A1104
@objc public protocol Colors {
public protocol Colors {
/// - Focused/Active states
/// - CTAs
var accent: UIColor { get }
var accent: DesignKitColorType { get }
/// - Error messages
/// - Content requiring user attention
/// - Notification, alerts
var alert: UIColor { get }
var alert: DesignKitColorType { get }
/// - Text
/// - Icons
var primaryContent: UIColor { get }
var primaryContent: DesignKitColorType { get }
/// - Text
/// - Icons
var secondaryContent: UIColor { get }
var secondaryContent: DesignKitColorType { get }
/// - Text
/// - Icons
var tertiaryContent: UIColor { get }
var tertiaryContent: DesignKitColorType { get }
/// - Text
/// - Icons
var quarterlyContent: UIColor { get }
var quarterlyContent: DesignKitColorType { get }
/// - Text
/// - Icons
var quinaryContent: UIColor { get }
var quinaryContent: DesignKitColorType { get }
/// Separating line
var separator: UIColor { get }
var separator: DesignKitColorType { get }
// Cards, tiles
var tile: UIColor { get }
var tile: DesignKitColorType { get }
/// Top navigation background on iOS
var navigation: UIColor { get }
var navigation: DesignKitColorType { get }
/// Background UI color
var background: UIColor { get }
var background: DesignKitColorType { get }
/// - Names in chat timeline
/// - Avatars default states that include first name letter
var namesAndAvatars: [UIColor] { get }
var namesAndAvatars: [DesignKitColorType] { get }
}

View File

@@ -15,69 +15,77 @@
//
import UIKit
import SwiftUI
public protocol DesignKitFontType { }
extension UIFont: DesignKitFontType { }
extension Font : DesignKitFontType { }
/// Describe fonts used in the application.
/// Font names are based on Element typograhy https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=1362%3A0 which is based on Apple font text styles (UIFont.TextStyle): https://developer.apple.com/documentation/uikit/uifonttextstyle
/// Create a custom TextStyle enum (like DesignKit.Fonts.TextStyle) is also a possiblity
@objc public protocol Fonts {
public protocol Fonts {
/// The font for large titles.
var largeTitle: UIFont { get }
var largeTitle: DesignKitFontType { get }
/// `largeTitle` with a Bold weight.
var largeTitleB: UIFont { get }
var largeTitleB: DesignKitFontType { get }
/// The font for first-level hierarchical headings.
var title1: UIFont { get }
var title1: DesignKitFontType { get }
/// `title1` with a Bold weight.
var title1B: UIFont { get }
var title1B: DesignKitFontType { get }
/// The font for second-level hierarchical headings.
var title2: UIFont { get }
var title2: DesignKitFontType { get }
/// `title2` with a Bold weight.
var title2B: UIFont { get }
var title2B: DesignKitFontType { get }
/// The font for third-level hierarchical headings.
var title3: UIFont { get }
var title3: DesignKitFontType { get }
/// `title3` with a Semi Bold weight.
var title3SB: UIFont { get }
var title3SB: DesignKitFontType { get }
/// The font for headings.
var headline: UIFont { get }
var headline: DesignKitFontType { get }
/// The font for subheadings.
var subheadline: UIFont { get }
var subheadline: DesignKitFontType { get }
/// The font for body text.
var body: UIFont { get }
var body: DesignKitFontType { get }
/// `body` with a Semi Bold weight.
var bodySB: UIFont { get }
var bodySB: DesignKitFontType { get }
/// The font for callouts.
var callout: UIFont { get }
var callout: DesignKitFontType { get }
/// `callout` with a Semi Bold weight.
var calloutSB: UIFont { get }
var calloutSB: DesignKitFontType { get }
/// The font for footnotes.
var footnote: UIFont { get }
var footnote: DesignKitFontType { get }
/// `footnote` with a Semi Bold weight.
var footnoteSB: UIFont { get }
var footnoteSB: DesignKitFontType { get }
/// The font for standard captions.
var caption1: UIFont { get }
var caption1: DesignKitFontType { get }
/// `caption1` with a Semi Bold weight.
var caption1SB: UIFont { get }
var caption1SB: DesignKitFontType { get }
/// The font for alternate captions.
var caption2: UIFont { get }
var caption2: DesignKitFontType { get }
/// `caption2` with a Semi Bold weight.
var caption2SB: UIFont { get }
var caption2SB: DesignKitFontType { get }
}

View File

@@ -5143,13 +5143,13 @@ extension VectorL10n {
static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
let format = NSLocalizedString(key, tableName: table, bundle: Bundle(for: BundleToken.self), comment: "")
let locale: Locale
if let localeIdentifier = Bundle.mxk_language() {
locale = Locale(identifier: localeIdentifier)
} else if let fallbackLocaleIdentifier = Bundle.mxk_fallbackLanguage() {
locale = Locale(identifier: fallbackLocaleIdentifier)
} else {
// if let localeIdentifier = Bundle.mxk_language() {
// locale = Locale(identifier: localeIdentifier)
// } else if let fallbackLocaleIdentifier = Bundle.mxk_fallbackLanguage() {
// locale = Locale(identifier: fallbackLocaleIdentifier)
// } else {
locale = Locale.current
}
// }
return String(format: format, locale: locale, arguments: args)
}

View File

@@ -0,0 +1,22 @@
//
// Copyright 2021 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 Foundation
enum AvatarInputOption {
case swiftUI(AvatarInputType)
case uiKit(AvatarViewDataProtocol)
}

View File

@@ -19,14 +19,6 @@ import MatrixSDK
import Combine
import DesignKit
/**
Provides a simple api to retrieve and cache avatar images
*/
protocol AvatarServiceType {
@available(iOS 14.0, *)
func avatarImage(mxContentUri: String, avatarSize: AvatarSize) -> Future<UIImage, Error>
}
enum AvatarServiceError: Error {
case pathNotfound
case loadingImageFailed(Error?)

View File

@@ -17,7 +17,7 @@
import Foundation
/// AvatarViewDataProtocol describe a view data that should be given to an AvatarView sublcass
protocol AvatarViewDataProtocol {
protocol AvatarViewDataProtocol: AvatarType {
/// Matrix item identifier (user id or room id)
var matrixItemId: String { get }

View File

@@ -0,0 +1,84 @@
//
// Copyright 2021 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 Foundation
fileprivate extension MXPushRule {
/*
Given a rule, check it match the actions in the static definition.
*/
private func maches(targetRule: NotificationStandardActions?) -> Bool {
guard let targetRule = targetRule else {
return false
}
if !enabled && targetRule == .disabled {
return true
}
if enabled,
let actions = targetRule.actions,
highlight == actions.highlight,
sound == actions.sound,
notify == actions.notify,
dontNotify == !actions.notify {
return true
}
return false
}
func getAction(actionType: MXPushRuleActionType, tweakType: String? = nil) -> MXPushRuleAction? {
guard let actions = actions as? [MXPushRuleAction] else {
return nil
}
return actions.first { action in
var match = action.actionType == actionType
MXLog.debug("action \(action)")
if let tweakType = tweakType,
let actionTweak = action.parameters?["set_tweak"] as? String {
match = match && (tweakType == actionTweak)
}
return match
}
}
var highlight: Bool {
guard let action = getAction(actionType: MXPushRuleActionTypeSetTweak, tweakType: "highlight") else {
return false
}
if let highlight = action.parameters["value"] as? Bool {
return highlight
}
return true
}
var sound: String? {
guard let action = getAction(actionType: MXPushRuleActionTypeSetTweak, tweakType: "sound") else {
return nil
}
return action.parameters["value"] as? String
}
var notify: Bool {
return getAction(actionType: MXPushRuleActionTypeNotify) != nil
}
var dontNotify: Bool {
return getAction(actionType: MXPushRuleActionTypeDontNotify) != nil
}
}

View File

@@ -17,45 +17,6 @@
import Foundation
import Combine
/**
A service for changing notification settings and keywords
*/
@available(iOS 14.0, *)
protocol NotificationSettingsServiceType {
/**
Publisher of all push rules.
*/
var rulesPublisher: AnyPublisher<[MXPushRule], Never> { get }
/**
Publisher of content rules.
*/
var contentRulesPublisher: AnyPublisher<[MXPushRule], Never> { get }
/**
Adds a keyword.
- Parameters:
- keyword: The keyword to add.
- enabled: Whether the keyword should be added in the enabled or disabled state.
*/
func add(keyword: String, enabled: Bool)
/**
Removes a keyword.
- Parameters:
- keyword: The keyword to remove.
*/
func remove(keyword: String)
/**
Updates the push rule actions.
- Parameters:
- ruleId: The id of the rule.
- enabled: Whether the rule should be enabled or disabled.
- actions: The actions to update with.
*/
func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?)
}
@available(iOS 14.0, *)
class NotificationSettingsService: NotificationSettingsServiceType {

View File

@@ -17,6 +17,7 @@
import Foundation
import Combine
import DesignKit
import UIKit
@available(iOS 14.0, *)
class MockAvatarService: AvatarServiceType {

View File

@@ -16,7 +16,7 @@
import Foundation
protocol AvatarInputType {
protocol AvatarInputType: AvatarType {
var mxContentUri: String? { get }
var matrixItemId: String { get }
var displayName: String? { get }
@@ -27,8 +27,3 @@ struct AvatarInput: AvatarInputType {
var matrixItemId: String
let displayName: String?
}
enum AvatarInputOption {
case swiftUI(AvatarInputType)
case uiKit(AvatarViewDataProtocol)
}

View File

@@ -0,0 +1,19 @@
//
// Copyright 2021 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 Foundation
protocol AvatarType { }

View File

@@ -29,3 +29,8 @@ class ThemeObserver: ObservableObject {
}
@Published var theme: Theme = ThemeService.shared().theme
}
class ThemePublisher: ObservableObject {
static let shared = ThemePublisher()
}

View File

@@ -0,0 +1,28 @@
//
// Copyright 2021 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 Foundation
import DesignKit
import Combine
import UIKit
/**
Provides a simple api to retrieve and cache avatar images
*/
protocol AvatarServiceType {
@available(iOS 14.0, *)
func avatarImage(mxContentUri: String, avatarSize: AvatarSize) -> Future<UIImage, Error>
}

View File

@@ -47,7 +47,7 @@ class AvatarViewModel: InjectableObject, ObservableObject {
avatarService.avatarImage(mxContentUri: mxContentUri, avatarSize: avatarSize)
.sink { completion in
guard case let .failure(error) = completion else { return }
MXLog.error("[AvatarService] Failed to retrieve avatar: \(error)")
// MXLog.error("[AvatarService] Failed to retrieve avatar: \(error)")
// TODO: Report non-fatal error when we have Sentry or similar.
} receiveValue: { image in
self.viewState = .avatar(image)

View File

@@ -15,7 +15,7 @@
//
import Foundation
import UIKit
enum AvatarViewState {
case empty
case placeholder(String, Int)

View File

@@ -75,7 +75,7 @@ struct RoomNotificationSettings_Previews: PreviewProvider {
static let mockViewModel = RoomNotificationSettingsSwiftUIViewModel(
roomNotificationService: MockRoomNotificationSettingsService.example,
avatarData: .swiftUI(MockAvatarInput.example),
avatarData: MockAvatarInput.example,
displayName: MockAvatarInput.example.displayName,
roomEncrypted: true
)

View File

@@ -55,7 +55,7 @@ class RoomNotificationSettingsViewModel: RoomNotificationSettingsViewModelType {
convenience init(
roomNotificationService: RoomNotificationSettingsServiceType,
avatarData: AvatarInputOption?,
avatarData: AvatarType?,
displayName: String?,
roomEncrypted: Bool
) {

View File

@@ -23,7 +23,7 @@ struct RoomNotificationSettingsViewState: RoomNotificationSettingsViewStateType
let roomEncrypted: Bool
var saving: Bool
var notificationState: RoomNotificationState
var avatarData: AvatarInputOption?
var avatarData: AvatarType?
var displayName: String?
}
@@ -42,7 +42,7 @@ protocol RoomNotificationSettingsViewStateType {
var roomEncrypted: Bool { get }
var notificationOptions: [RoomNotificationState] { get }
var notificationState: RoomNotificationState { get }
var avatarData: AvatarInputOption? { get }
var avatarData: AvatarType? { get }
var displayName: String? { get }
}

View File

@@ -0,0 +1,25 @@
//
// Copyright 2021 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 Foundation
struct MockNotificationPushRule: NotificationPushRule {
var ruleId: String
var enabled: Bool
func matches(standardActions: NotificationStandardActions?) -> Bool {
return false
}
}

View File

@@ -22,10 +22,10 @@ class MockNotificationSettingsService: NotificationSettingsServiceType, Observab
static let example = MockNotificationSettingsService()
@Published var keywords = Set<String>()
@Published var rules = [MXPushRule]()
@Published var contentRules = [MXPushRule]()
@Published var rules = [NotificationPushRule]()
@Published var contentRules = [NotificationPushRule]()
var contentRulesPublisher: AnyPublisher<[MXPushRule], Never> {
var contentRulesPublisher: AnyPublisher<[NotificationPushRule], Never> {
$contentRules.eraseToAnyPublisher()
}
@@ -33,7 +33,7 @@ class MockNotificationSettingsService: NotificationSettingsServiceType, Observab
$keywords.eraseToAnyPublisher()
}
var rulesPublisher: AnyPublisher<[MXPushRule], Never> {
var rulesPublisher: AnyPublisher<[NotificationPushRule], Never> {
$rules.eraseToAnyPublisher()
}

View File

@@ -0,0 +1,23 @@
//
// Copyright 2021 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 Foundation
protocol NotificationPushRule {
var ruleId: String { get }
var enabled: Bool { get }
func matches(standardActions: NotificationStandardActions?) -> Bool
}

View File

@@ -56,7 +56,7 @@ struct BorderedInputFieldStyle: TextFieldStyle {
}
private var backgroundColor: Color {
if !isEnabled && (theme is DarkTheme) {
if !isEnabled && (theme.identifier == ThemeIdentifier.dark.rawValue) {
return Color(theme.colors.quinaryContent)
}
return Color(theme.colors.background)

View File

@@ -34,7 +34,7 @@ struct FormInputFieldStyle: TextFieldStyle {
}
private var backgroundColor: Color {
if !isEnabled && (theme is DarkTheme) {
if !isEnabled && (theme.identifier == ThemeIdentifier.dark.rawValue) {
return Color(theme.colors.quinaryContent)
}
return Color(theme.colors.background)

View File

@@ -0,0 +1,57 @@
//
// Copyright 2021 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 Foundation
import Combine
/**
A service for changing notification settings and keywords
*/
@available(iOS 14.0, *)
protocol NotificationSettingsServiceType {
/**
Publisher of all push rules.
*/
var rulesPublisher: AnyPublisher<[NotificationPushRule], Never> { get }
/**
Publisher of content rules.
*/
var contentRulesPublisher: AnyPublisher<[NotificationPushRule], Never> { get }
/**
Adds a keyword.
- Parameters:
- keyword: The keyword to add.
- enabled: Whether the keyword should be added in the enabled or disabled state.
*/
func add(keyword: String, enabled: Bool)
/**
Removes a keyword.
- Parameters:
- keyword: The keyword to remove.
*/
func remove(keyword: String)
/**
Updates the push rule actions.
- Parameters:
- ruleId: The id of the rule.
- enabled: Whether the rule should be enabled or disabled.
- actions: The actions to update with.
*/
func updatePushRuleActions(for ruleId: String, enabled: Bool, actions: NotificationActions?)
}

View File

@@ -155,7 +155,7 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob
}
// MARK: - Private
private func rulesUpdated(newRules: [MXPushRule]) {
private func rulesUpdated(newRules: [NotificationPushRule]) {
for rule in newRules {
guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId),
ruleIds.contains(ruleId) else { continue }
@@ -174,11 +174,11 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob
Matcing is done by comparing the rule against the static definitions for that rule.
The same logic is used on android.
*/
private func isChecked(rule: MXPushRule) -> Bool {
private func isChecked(rule: NotificationPushRule) -> Bool {
guard let ruleId = NotificationPushRuleId(rawValue: rule.ruleId) else { return false }
let firstIndex = NotificationIndex.allCases.first { nextIndex in
return ruleMaches(rule: rule, targetRule: ruleId.standardActions(for: nextIndex))
return rule.matches(standardActions: ruleId.standardActions(for: nextIndex))
}
guard let index = firstIndex else {
@@ -187,69 +187,5 @@ final class NotificationSettingsViewModel: NotificationSettingsViewModelType, Ob
return index.enabled
}
/*
Given a rule, check it match the actions in the static definition.
*/
private func ruleMaches(rule: MXPushRule, targetRule: NotificationStandardActions?) -> Bool {
guard let targetRule = targetRule else {
return false
}
if !rule.enabled && targetRule == .disabled {
return true
}
if rule.enabled,
let actions = targetRule.actions,
rule.highlight == actions.highlight,
rule.sound == actions.sound,
rule.notify == actions.notify,
rule.dontNotify == !actions.notify {
return true
}
return false
}
}
fileprivate extension MXPushRule {
func getAction(actionType: MXPushRuleActionType, tweakType: String? = nil) -> MXPushRuleAction? {
guard let actions = actions as? [MXPushRuleAction] else {
return nil
}
return actions.first { action in
var match = action.actionType == actionType
MXLog.debug("action \(action)")
if let tweakType = tweakType,
let actionTweak = action.parameters?["set_tweak"] as? String {
match = match && (tweakType == actionTweak)
}
return match
}
}
var highlight: Bool {
guard let action = getAction(actionType: MXPushRuleActionTypeSetTweak, tweakType: "highlight") else {
return false
}
if let highlight = action.parameters["value"] as? Bool {
return highlight
}
return true
}
var sound: String? {
guard let action = getAction(actionType: MXPushRuleActionTypeSetTweak, tweakType: "sound") else {
return nil
}
return action.parameters["value"] as? String
}
var notify: Bool {
return getAction(actionType: MXPushRuleActionTypeNotify) != nil
}
var dontNotify: Bool {
return getAction(actionType: MXPushRuleActionTypeDontNotify) != nil
}
}

View File

@@ -26,6 +26,18 @@ schemes:
- RiotTests
targets:
RiotSwiftUI:
type: framework
platform: iOS
dependencies:
- target: DesignKit
sources:
- path: ModulesSwiftUI
- path: Generated/Strings.swift
- path: Generated/Images.swift
- path: Managers/Theme/Theme.swift
- path: Managers/Theme/ThemeIdentifier.swift
- path: Categories/UIColor.swift
Riot:
type: application
platform: iOS
@@ -35,6 +47,7 @@ targets:
- target: SiriIntents
- target: RiotNSE
- target: DesignKit
- target: RiotSwiftUI
configFiles:
Debug: Debug.xcconfig