Skip to content

Commit

Permalink
Merge branch 'develop' into mauroromito/enabling_push_notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
Velin92 committed Apr 5, 2023
2 parents 4f432aa + 8a56649 commit 3ba8b3e
Show file tree
Hide file tree
Showing 17 changed files with 664 additions and 199 deletions.
37 changes: 37 additions & 0 deletions .github/workflows/translations-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Open Translations PR
on:
schedule:
# At 00:00 on every Monday UTC
- cron: '0 0 * * 1'
workflow_dispatch:

jobs:
open-translations-pr:
runs-on: macos-12
# Skip in forks
if: github.repository == 'vector-im/element-x-ios'
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Setup Localazy and Mint
run: brew install mint localazy/tools/localazy

- name: Download All Translations
run: swift run tools download-strings --all-languages

- name: Verify Translations
run: swift run tools locheck

- name: Create PR for Translations
uses: peter-evans/create-pull-request@v5
with:
commit-message: Translations update
title: Translations update
body: |
- Translations update
branch: translations/update
base: develop
add-paths: |
*.strings
*.stringsdict
281 changes: 281 additions & 0 deletions ElementX/Resources/Localizations/es.lproj/Localizable.strings

Large diffs are not rendered by default.

54 changes: 54 additions & 0 deletions ElementX/Resources/Localizations/es.lproj/Localizable.stringsdict
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>common_member_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@COUNT@</string>
<key>COUNT</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%1$d miembro</string>
<key>other</key>
<string>%1$d miembros</string>
</dict>
</dict>
<key>room_timeline_state_changes</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@COUNT@</string>
<key>COUNT</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%1$d cambio en la sala</string>
<key>other</key>
<string>%1$d cambios en la sala</string>
</dict>
</dict>
<key>screen_room_member_list_header_title</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@COUNT@</string>
<key>COUNT</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Una persona</string>
<key>other</key>
<string>%1$d personas</string>
</dict>
</dict>
</dict>
</plist>
6 changes: 3 additions & 3 deletions ElementX/Resources/Localizations/it.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"common_about" = "Informazioni";
"common_audio" = "Audio";
"common_bubbles" = "Fumetti";
"common_creating_room" = "Creazione stanza...";
"common_creating_room" = "Creazione stanza";
"common_current_user_left_room" = "Hai lasciato la stanza";
"common_decryption_error" = "Errore di decrittazione";
"common_developer_options" = "Opzioni sviluppatore";
Expand Down Expand Up @@ -141,7 +141,7 @@
"screen_bug_report_contact_me" = "Potete contattarmi per qualsiasi altra domanda";
"screen_bug_report_edit_screenshot" = "Modifica istantanea schermo";
"screen_bug_report_editor_description" = "Descrivi il bug. Che cosa hai fatto? Cosa ti aspettavi che accadesse? Cosa è effettivamente accaduto. Si prega di inserire il maggior numero di dettagli possibile.";
"screen_bug_report_editor_placeholder" = "Descrivi il problema...";
"screen_bug_report_editor_placeholder" = "Descrivi il problema";
"screen_bug_report_editor_supporting" = "Se possibile, scrivere la descrizione in inglese.";
"screen_bug_report_include_crash_logs" = "Invia i log degli arresti anomali";
"screen_bug_report_include_logs" = "Invia i log per aiutarci";
Expand Down Expand Up @@ -213,7 +213,7 @@
"screen_signout_confirmation_dialog_content" = "Sei sicuro di voler uscire?";
"screen_signout_confirmation_dialog_submit" = "Esci";
"screen_signout_confirmation_dialog_title" = "Esci";
"screen_signout_in_progress_dialog_content" = "Uscita in corso...";
"screen_signout_in_progress_dialog_content" = "Uscita in corso";
"screen_signout_preference_item" = "Esci";
"screen_start_chat_error_starting_chat" = "Si è verificato un errore durante il tentativo di avviare una chat";
"screen_start_chat_unknown_profile" = "Non possiamo convalidare l'ID Matrix di questo utente. L'invito potrebbe non essere ricevuto.";
Expand Down
266 changes: 133 additions & 133 deletions ElementX/Resources/Localizations/ro.lproj/Localizable.strings

Large diffs are not rendered by default.

99 changes: 46 additions & 53 deletions ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,49 +17,6 @@
import Combine
import Foundation

/// A constrained and concise interface for interacting with the ViewModel.
///
/// This class is closely bound to`StateStoreViewModel`. It provides the exact interface the view should need to interact
/// ViewModel (as modelled on our previous template architecture with the addition of two-way binding):
/// - The ability read/observe view state
/// - The ability to send view events
/// - The ability to bind state to a specific portion of the view state safely.
/// This class was brought about a little bit by necessity. The most idiomatic way of interacting with SwiftUI is via `@Published`
/// properties which which are property wrappers and therefore can't be defined within protocols.
/// A similar approach is taken in libraries like [CombineFeedback](https://github.com/sergdort/CombineFeedback).
/// It provides a nice layer of consistency and also safety. As we are not passing the `ViewModel` to the view directly, shortcuts/hacks
/// can't be made into the `ViewModel`.
@dynamicMemberLookup
@MainActor
class ViewModelContext<ViewState: BindableState, ViewAction>: ObservableObject {
fileprivate let viewActions: PassthroughSubject<ViewAction, Never>

/// Get-able/Observable `Published` property for the `ViewState`
@Published fileprivate(set) var viewState: ViewState

/// An optional image loading service so that views can manage themselves
/// Intentionally non-generic so that it doesn't grow uncontrollably
let imageProvider: ImageProviderProtocol?

/// Set-able/Bindable access to the bindable state.
subscript<T>(dynamicMember keyPath: WritableKeyPath<ViewState.BindStateType, T>) -> T {
get { viewState.bindings[keyPath: keyPath] }
set { viewState.bindings[keyPath: keyPath] = newValue }
}

init(initialViewState: ViewState, imageProvider: ImageProviderProtocol?) {
self.viewActions = PassthroughSubject()
self.viewState = initialViewState
self.imageProvider = imageProvider
}

/// Send a `ViewAction` to the `ViewModel` for processing.
/// - Parameter viewAction: The `ViewAction` to send to the `ViewModel`.
func send(viewAction: ViewAction) {
viewActions.send(viewAction)
}
}

/// A common ViewModel implementation for handling of `State` and `ViewAction`s
///
/// Generic type State is constrained to the BindableState protocol in that it may contain (but doesn't have to)
Expand All @@ -68,35 +25,71 @@ class ViewModelContext<ViewState: BindableState, ViewAction>: ObservableObject {
/// we can do it in this centralised place.
@MainActor
class StateStoreViewModel<State: BindableState, ViewAction> {
typealias Context = ViewModelContext<State, ViewAction>

/// For storing subscription references.
///
/// Left as public for `ViewModel` implementations convenience.
var cancellables = Set<AnyCancellable>()

/// Constrained interface for passing to Views.
var context: Context

var state: State {
get { context.viewState }
set { context.viewState = newValue }
}

init(initialViewState: State, imageProvider: ImageProviderProtocol? = nil) {
context = Context(initialViewState: initialViewState, imageProvider: imageProvider)
context.viewActions
.sink { [weak self] action in
guard let self else { return }

self.process(viewAction: action)
}
.store(in: &cancellables)
context.viewModel = self
}

/// Override to handles incoming `ViewAction`s from the `ViewModel`.
/// - Parameter viewAction: The `ViewAction` to be processed in `ViewModel` implementation.
func process(viewAction: ViewAction) {
// Default implementation, -no-op
}

// MARK: - Context

/// A constrained and concise interface for interacting with the ViewModel.
///
/// This class is closely bound to`StateStoreViewModel`. It provides the exact interface the view should need to interact
/// ViewModel (as modelled on our previous template architecture with the addition of two-way binding):
/// - The ability read/observe view state
/// - The ability to send view events
/// - The ability to bind state to a specific portion of the view state safely.
/// This class was brought about a little bit by necessity. The most idiomatic way of interacting with SwiftUI is via `@Published`
/// properties which which are property wrappers and therefore can't be defined within protocols.
/// A similar approach is taken in libraries like [CombineFeedback](https://github.com/sergdort/CombineFeedback).
/// It provides a nice layer of consistency and also safety. As we are not passing the `ViewModel` to the view directly, shortcuts/hacks
/// can't be made into the `ViewModel`.
@dynamicMemberLookup
@MainActor
final class Context: ObservableObject {
fileprivate weak var viewModel: StateStoreViewModel?

/// Get-able/Observable `Published` property for the `ViewState`
@Published fileprivate(set) var viewState: State

/// An optional image loading service so that views can manage themselves
/// Intentionally non-generic so that it doesn't grow uncontrollably
let imageProvider: ImageProviderProtocol?

/// Set-able/Bindable access to the bindable state.
subscript<T>(dynamicMember keyPath: WritableKeyPath<State.BindStateType, T>) -> T {
get { viewState.bindings[keyPath: keyPath] }
set { viewState.bindings[keyPath: keyPath] = newValue }
}

/// Send a `ViewAction` to the `ViewModel` for processing.
/// - Parameter viewAction: The `ViewAction` to send to the `ViewModel`.
func send(viewAction: ViewAction) {
viewModel?.process(viewAction: viewAction)
}

fileprivate init(initialViewState: State, imageProvider: ImageProviderProtocol?) {
self.viewState = initialViewState
self.imageProvider = imageProvider
}
}
}
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "Element Swift",
platforms: [
.macOS(.v13)
.macOS(.v12)
],
products: [
.executable(name: "tools", targets: ["Tools"])
Expand Down
4 changes: 2 additions & 2 deletions Tools/Sources/BuildSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ struct BuildSDK: ParsableCommand {
var profile: Profile = .debug

private var parentDirectoryURL: URL { Utilities.projectDirectoryURL.deletingLastPathComponent() }
private var sdkDirectoryURL: URL { parentDirectoryURL.appending(path: "matrix-rust-sdk") }
private var sdkDirectoryURL: URL { parentDirectoryURL.appendingPathComponent("matrix-rust-sdk") }

enum Error: LocalizedError {
case rustupOutputFailure
Expand Down Expand Up @@ -109,7 +109,7 @@ struct BuildSDK: ParsableCommand {

/// Update project.yml with the local path of the SDK.
func updateProjectYAML() throws {
let yamlURL = Utilities.projectDirectoryURL.appending(path: "project.yml")
let yamlURL = Utilities.projectDirectoryURL.appendingPathComponent("project.yml")
let yamlString = try String(contentsOf: yamlURL)
guard var projectConfig = try Yams.compose(yaml: yamlString) else { throw Error.failureParsingProjectYAML }

Expand Down
18 changes: 18 additions & 0 deletions Tools/Sources/DownloadStrings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import ArgumentParser
import Foundation

struct DownloadStrings: ParsableCommand {
static var configuration = CommandConfiguration(abstract: "A tool to download localizable strings from localazy")

@Flag(help: "Use to download translation keys for all languages")
var allLanguages = false

func run() throws {
try localazyDownload()
}

private func localazyDownload() throws {
let json = allLanguages ? "localazy-all.json" : "localazy-en.json"
try Utilities.zsh("localazy download --config \(json)")
}
}
44 changes: 44 additions & 0 deletions Tools/Sources/Locheck.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import ArgumentParser
import Foundation

struct Locheck: ParsableCommand {
enum LocheckError: LocalizedError {
case missingMint
case outputError

var errorDescription: String? {
switch self {
case .missingMint:
return "💥 Unable to find mint. Fix by running:\nbrew install mint\n"
case .outputError:
return "💥 Failed to read the output from locheck."
}
}
}

static var configuration = CommandConfiguration(abstract: "A tool that verifies bad strings contained in localization files")

private var stringsDirectoryURL: URL {
Utilities.projectDirectoryURL.appendingPathComponent("ElementX/Resources/Localizations")
}

func run() throws {
try checkMint()
try checkStrings()
}

func checkStrings() throws {
guard let output = try Utilities.zsh("mint run locheck discoverlproj --ignore-missing --ignore lproj_file_missing_from_translation --treat-warnings-as-errors \(stringsDirectoryURL.path)") else {
throw LocheckError.missingMint
}
print(output)
}

private func checkMint() throws {
let result = try Utilities.zsh("which mint")

if result?.contains("not found") == true {
throw LocheckError.missingMint
}
}
}
2 changes: 1 addition & 1 deletion Tools/Sources/OutdatedPackages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Foundation
struct OutdatedPackages: ParsableCommand {
static var configuration = CommandConfiguration(abstract: "A tool to check outdated package dependencies. Please make sure you have already run setup-project before using this tool.")

private var projectSwiftPMDirectoryURL: URL { Utilities.projectDirectoryURL.appending(path: "ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm") }
private var projectSwiftPMDirectoryURL: URL { Utilities.projectDirectoryURL.appendingPathComponent("ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm") }

func run() throws {
try checkToolsDependencies()
Expand Down
2 changes: 1 addition & 1 deletion Tools/Sources/SetupProject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct SetupProject: ParsableCommand {
}

func brewBundleInstall() throws {
try Utilities.zsh("brew install xcodegen swiftgen swiftlint swiftformat git-lfs sourcery kiliankoe/formulae/swift-outdated localazy/tools/localazy")
try Utilities.zsh("brew install xcodegen swiftgen swiftlint swiftformat git-lfs sourcery mint kiliankoe/formulae/swift-outdated localazy/tools/localazy")
}

func xcodegen() throws {
Expand Down
4 changes: 3 additions & 1 deletion Tools/Sources/Tools.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ struct Tools: ParsableCommand {
static var configuration = CommandConfiguration(abstract: "A collection of command line tools for ElementX",
subcommands: [BuildSDK.self,
SetupProject.self,
OutdatedPackages.self])
OutdatedPackages.self,
DownloadStrings.self,
Locheck.self])
}
4 changes: 2 additions & 2 deletions Tools/Sources/Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ enum Utilities {
}
}

static var projectDirectoryURL: URL { URL(filePath: FileManager.default.currentDirectoryPath) }
static var projectDirectoryURL: URL { URL(fileURLWithPath: FileManager.default.currentDirectoryPath) }

/// Runs a command in zsh.
@discardableResult
static func zsh(_ command: String, workingDirectoryURL: URL = projectDirectoryURL) throws -> String? {
let process = Process()
process.executableURL = URL(filePath: "/bin/zsh")
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
process.arguments = ["-cu", command]
process.currentDirectoryURL = workingDirectoryURL

Expand Down
4 changes: 2 additions & 2 deletions UnitTests/Sources/Extensions/ViewModelContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@

@testable import ElementX

extension ViewModelContext {
extension StateStoreViewModel.Context {
@discardableResult
func nextViewState() async -> ViewState? {
func nextViewState() async -> State? {
await $viewState.nextValue
}
}
File renamed without changes.
Loading

0 comments on commit 3ba8b3e

Please sign in to comment.