Compare commits
303 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd2e34fb63 | |||
| bdf74e89f3 | |||
| 65cc9ef940 | |||
| a838a03a1f | |||
| 7587fd76ee | |||
| b475cc7e9a | |||
| 93149f1cbd | |||
| 352d25f2bb | |||
| 00f08ab2dc | |||
| 07918562dd | |||
| 33d04f0303 | |||
| 34523cf51a | |||
| 8bfd4868c0 | |||
| fa409739c8 | |||
| 4e8774a9a8 | |||
| 92f276e20f | |||
| 394d6f269c | |||
| 116f716d3a | |||
| 65d19d3274 | |||
| d5eace30ed | |||
| 6c9df17d12 | |||
| 44909589b8 | |||
| b00095aaa8 | |||
| d7ad2f1dad | |||
| 2c1270ec79 | |||
| e57d872fc8 | |||
| 9fe64b7cd4 | |||
| b652dfe69a | |||
| 8401c2612a | |||
| 426fdc70a1 | |||
| 0454916b65 | |||
| ac183daff8 | |||
| 327ca090c5 | |||
| ef15fd0666 | |||
| 0cff8bd98e | |||
| e32e1dd388 | |||
| 098edab1b4 | |||
| 15627e4512 | |||
| 1af16e92db | |||
| 346a1e8ebb | |||
| 350d47c94a | |||
| c5675eeb9b | |||
| f224c104e0 | |||
| b4395d948f | |||
| 4355307ba0 | |||
| 7052751e38 | |||
| 491f7c538e | |||
| 2ae8b9bf75 | |||
| 6839258111 | |||
| 8752897bc3 | |||
| fadd19d847 | |||
| eec2facab1 | |||
| 86bc4d54b6 | |||
| b4a36ad465 | |||
| 53df7dd24d | |||
| aa4e116e01 | |||
| 9b22ca3cd2 | |||
| 32f86da98d | |||
| 9f7e30820b | |||
| efefd549a2 | |||
| 600b9224b3 | |||
| 2b9ec0336d | |||
| b6a7188fb5 | |||
| 6292ef6bfe | |||
| 8874ee112c | |||
| 142529ad69 | |||
| f6f6538bd6 | |||
| a139257967 | |||
| ca1d44a53d | |||
| fd72779f00 | |||
| fbb5632896 | |||
| c812b723dc | |||
| c8b12d9496 | |||
| 353233672a | |||
| 59ef54974d | |||
| 9c65d36101 | |||
| 5481f35727 | |||
| 7ecb7f89e6 | |||
| d611bad03b | |||
| 328efce428 | |||
| 313f250273 | |||
| ae4620d7d6 | |||
| ec105aadf5 | |||
| 024b02324e | |||
| b864918686 | |||
| 925e61e40c | |||
| 8ead25eaa3 | |||
| 84556c0672 | |||
| 1ba090b914 | |||
| c65147678a | |||
| 70714dd7f7 | |||
| d360d59b0b | |||
| f454dd593c | |||
| 6945329581 | |||
| 0d222e8ac0 | |||
| dd84da15b0 | |||
| 8a3468658b | |||
| 72d55e485a | |||
| 0bd571d068 | |||
| 0eea289b89 | |||
| 9dd9fd9176 | |||
| e7787f4c3f | |||
| 8c9edd5ea9 | |||
| bb0e80ba16 | |||
| 7edafe6b35 | |||
| a90ca1ff22 | |||
| 2a808f9b58 | |||
| cd453bc6fa | |||
| 771d10ac6b | |||
| 5f49c25952 | |||
| 804fccf650 | |||
| 864d022f45 | |||
| b27a549ef8 | |||
| bf2561f937 | |||
| fa2b13864a | |||
| 7ce16c5b2d | |||
| f3c8438bd8 | |||
| 64076c6cf9 | |||
| 28966c8397 | |||
| 8bc7e7c281 | |||
| 404410b8a2 | |||
| 1aa9b29eb5 | |||
| 033cb2e038 | |||
| d3b068cbad | |||
| 55a1a21f08 | |||
| 360d6d0ee1 | |||
| f090f30e7e | |||
| 224edafabd | |||
| 809965f693 | |||
| 5f236c577f | |||
| f7f55024ab | |||
| 1db7a9b07c | |||
| c62aed4e0c | |||
| df6b7d0d21 | |||
| 5cb4a2fe5a | |||
| b41d801cab | |||
| d4d74dd8f8 | |||
| 9f648803f9 | |||
| b6239bd53c | |||
| 99f03221c9 | |||
| 78563ef09e | |||
| c0e19454e3 | |||
| d595e4fd83 | |||
| f73285db3e | |||
| cbe17409bb | |||
| e302b37b6a | |||
| 5866c4340a | |||
| 9c0d17c79c | |||
| 37c78bf6b1 | |||
| 4de42be688 | |||
| 8753fe3980 | |||
| 5724e65d8e | |||
| 1febb3ef91 | |||
| 10867d1246 | |||
| 0b0b8a96ff | |||
| 947a47943b | |||
| d3b12561b3 | |||
| 2228da3eb9 | |||
| d57c376b21 | |||
| 3f6347b6c9 | |||
| 189a04b94b | |||
| 6efa8743ff | |||
| 42a7b6e8ee | |||
| b829c6a06f | |||
| 66f68093f0 | |||
| bb0713b686 | |||
| 0856fdc146 | |||
| 45890c71a2 | |||
| 3f6021bcd1 | |||
| 76c34e29bb | |||
| e3be2981ff | |||
| 03c434074a | |||
| aeffd942ec | |||
| 1a72a4e17b | |||
| bbdbbe1081 | |||
| 668fbe1827 | |||
| 58e1a0212d | |||
| 39805d080e | |||
| a82726a991 | |||
| a0521d1fdf | |||
| 8684b2c050 | |||
| d7283aa552 | |||
| f422208399 | |||
| d0fca495cb | |||
| 0bddf30de9 | |||
| 748ec818ac | |||
| 80530f0006 | |||
| 68f7f2b301 | |||
| 5672b5cfcc | |||
| f31207583e | |||
| 56b790da74 | |||
| b2d74abf2f | |||
| ee8223f0a9 | |||
| c4e5bde5bf | |||
| 4ca7c1a8fa | |||
| eabbeffd09 | |||
| 1e78825b0b | |||
| 3ab0aacabf | |||
| 1407a1d5b4 | |||
| 56d22104ad | |||
| d7127562d7 | |||
| 9a1db7e664 | |||
| 58938dbd4f | |||
| ad38d9698a | |||
| 7c245acaaf | |||
| bbc995e2c9 | |||
| 94cd9590b5 | |||
| f5a1a7d952 | |||
| 3eb8075913 | |||
| e498271420 | |||
| dbb14e0df4 | |||
| e32c534b89 | |||
| b044764310 | |||
| 68a277143a | |||
| d8cb49e892 | |||
| 5d47bb3c56 | |||
| 6d77a0a557 | |||
| 06753c7748 | |||
| 894b8edc2c | |||
| 743c22d980 | |||
| ae795df530 | |||
| 33f8ce2759 | |||
| 704b3dbb29 | |||
| 58fb7c4fcb | |||
| c296eb0e43 | |||
| 55bbc301ef | |||
| 30bc4a06f2 | |||
| 7ea9aeb174 | |||
| b4f397ed6a | |||
| 8c56ecbd04 | |||
| e62002e7fc | |||
| 4d7d61dae7 | |||
| a8c79fdd46 | |||
| 5262350b40 | |||
| 27928480d5 | |||
| 274da99787 | |||
| 5d0343c480 | |||
| 4a9cd02fbc | |||
| 3796fdc966 | |||
| ae71a92ab7 | |||
| d4849a2a79 | |||
| bf498a5d18 | |||
| 1770e1d815 | |||
| 6967f99626 | |||
| cff46137b9 | |||
| e98a19eb3f | |||
| 0f72fe3511 | |||
| 94313173f0 | |||
| b46189d05f | |||
| e4f919e58f | |||
| c0385ccdfa | |||
| 6f8c5a0e76 | |||
| da6eca78f0 | |||
| 4eb0863f3a | |||
| 32a54c5b71 | |||
| a6466e80e0 | |||
| 4f03acab0c | |||
| b5b28b9986 | |||
| 711cb0a05c | |||
| ea6475fa1e | |||
| 6d74805a65 | |||
| 5d1e15f308 | |||
| 6a2ed331fa | |||
| 762e5ad0ff | |||
| 9a61d870d6 | |||
| 9354d9d0c2 | |||
| d20f62b11b | |||
| 9093631f8b | |||
| 175aca596e | |||
| e8fffe35d2 | |||
| 99ba9eb235 | |||
| 003c608cc5 | |||
| c4f38cf1b2 | |||
| 60cd7fd5cc | |||
| 5f6729b737 | |||
| 5198533242 | |||
| 814cb99105 | |||
| ce78d1999e | |||
| 7f5876c079 | |||
| 34c41cd527 | |||
| 3d84c611ae | |||
| 5e381e17ea | |||
| 63c99282de | |||
| 2f4dc6e3e4 | |||
| a0ec5ed9dd | |||
| a4a509f2de | |||
| edaac3eecd | |||
| 2f1cb3eace | |||
| 1158ce61fa | |||
| 5c2c002f3f | |||
| 3fa61c01b2 | |||
| f61c700092 | |||
| fd5b3813d4 | |||
| a2ba9c4999 | |||
| 9e2696a395 | |||
| 8bc676e2d0 | |||
| 4f29cf6b7a | |||
| 6e3a6b986a | |||
| fd6fb944ad | |||
| 0be5693779 | |||
| 1062e2c6dd | |||
| 39bd882a27 | |||
| 85f28d017d |
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: constrained-tests
|
||||
description: Slow-CI simulation — run unit tests on saturated CPU to reproduce CI-style flakiness locally. Suggest when new/reworked suite lean heavy on awaits (deferFulfillment, waitForConfirmation, TestClock, timeouts) or test fail only on CI. Not for every change.
|
||||
---
|
||||
|
||||
# Constrained test runs
|
||||
|
||||
CI runners shared + slow. Background tasks starve, awaits stretch, scheduler races flake there, pass locally. `--constrained` saturate CPU with hog tasks — same conditions, local.
|
||||
|
||||
## When
|
||||
|
||||
- PR add/rework suite heavy on timing-sensitive async: `deferFulfillment`, `waitForConfirmation`, `TestClock`, `transitionValues`, timeouts.
|
||||
- Test fail on CI, pass locally.
|
||||
- Not routine changes, not sync suites — slow, no gain.
|
||||
|
||||
## How
|
||||
|
||||
Full unit test workflow (what CI run):
|
||||
|
||||
```bash
|
||||
swift run tools ci unit-tests --constrained
|
||||
```
|
||||
|
||||
One suite, iterate on new tests (prefer while developing):
|
||||
|
||||
```bash
|
||||
swift run tools ci run-tests --scheme UnitTests --test-name MyNewTests --constrained
|
||||
```
|
||||
|
||||
Hog count automatic (2× cores). Suite survive ~10 constrained iterations → good shape.
|
||||
|
||||
## Failures mean
|
||||
|
||||
Failure under constraint = real bug in test (sometimes code under test): assertion race async work, transient state observation missed, timeout too tight. Fix synchronisation, no blind timeout bump. Helpers in `UnitTests/Sources/TestUtilities/` (`deferFulfillment`, `deferFailure`, `waitForConfirmation`). Wait on durable state, not transient.
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: periphery-cleanup
|
||||
description: Dead-code sweep with Periphery — scan the Pro-configured project, delete unused code, guard false positives (keep-alive handles, future-use code), run unit tests, purge scan cache. Use when user want "clean up dead code", "run periphery", "remove unused code".
|
||||
---
|
||||
|
||||
# Periphery cleanup
|
||||
|
||||
Periphery = static analysis, find unused code. Scan build ALL schemes — slow (cold ~30-60 min). Maintenance thing, run rare.
|
||||
|
||||
## Scan — always on the Pro variant
|
||||
|
||||
Scan run on the Pro-configured project so every source get analysed (plain config leave hook implementations + their call sites out, flood report with false "unused"):
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive # once, if submodules missing
|
||||
swift run pipeline configure-element-pro
|
||||
periphery scan --relative-results --quiet > periphery-report.txt
|
||||
```
|
||||
|
||||
Config live in `.periphery.yml` — schemes already cover every target, don't narrow. Two deliberate settings there:
|
||||
- `retain_unused_protocol_func_params: true` — protocol signatures are contracts (hooks, delegates); params unused in every conformance stay. NEVER annotate protocol function parameters, config already handle them.
|
||||
- `index_exclude: compound-ios/**` — Compound excluded: its Inspector app + package tests live outside this project's schemes, so their usage invisible → findings there unreliable (e.g. making things internal break the Inspector). Never act on Compound code from this scan.
|
||||
|
||||
Report line format: `path:line:col: warning: <kind> '<name>'`. Build fail → fix build first, scan need compiling project.
|
||||
|
||||
## Comment hygiene — strip, scan, restore
|
||||
|
||||
Every run: existing `periphery:ignore` comments hide drift — code change, comment stay, nobody notice it useless. So:
|
||||
|
||||
1. **Strip + remember.** Before the scan, remove every `periphery:ignore` comment (both spellings: `periphery:ignore` and `periphery: ignore`) and stash them: `git diff > /tmp/periphery-comments.patch` right after stripping, or just rely on `git diff` against HEAD to recall what was where.
|
||||
2. **Scan.** The report now show ground truth: every declaration that genuinely need protection re-flag.
|
||||
3. **Restore only what re-flag.** For each finding that match the false-positive table, put the comment back (same reason strings). Findings that DON'T re-flag = comments that were stale → stay deleted. Anything newly flagged that isn't a false positive = real dead code → delete it.
|
||||
|
||||
Result: comment set always minimal, always true. The clutter never accumulate.
|
||||
|
||||
## Triage — kind → action
|
||||
|
||||
| Warning | Action |
|
||||
|---|---|
|
||||
| Unused function / property / enum / struct / init | Check false-positive table FIRST. True dead → delete whole declaration + helpers/extensions only it used |
|
||||
| Assign-only property | DANGER ZONE. Prime keep-alive suspect — read table below before touch |
|
||||
| Unused parameter | Protocol params never appear (config retain them). ObjC selector / system callback → `ignore:parameters <name> - <reason>`. Free signature → remove param + fix call sites |
|
||||
| Redundant protocol conformance | `TestablePreview` → SKIP, always (see below). Others: verify nothing need conformance (generics, tests, mocks), then remove conformance only, keep type |
|
||||
| Redundant public accessibility | Drop `public`, make internal — EXCEPT member that satisfy a public protocol in a public type (e.g. `previews`), Swift refuse internal there; skip or internalise whole type |
|
||||
| Superfluous ignore comment | Declaration referenced now — comment dead, code alive. Remove comment ONLY, never declaration |
|
||||
|
||||
Never touch `Generated/` directories, never edit `Localizable.strings`. Delete bottom-up per file — line numbers stay valid.
|
||||
|
||||
## False positives — mark, no delete
|
||||
|
||||
Stored-never-read variable often alive ON PURPOSE: keep task / SDK object / coordinator alive past declaration scope. Delete = app break silent (observation stop, flow deallocate, rust handle drop). Mark instead: `// periphery:ignore - <reason>` line above declaration (NEVER `periphery:ignore:all` — whole files that shouldn't be reported, like hand-written mocks, belong in `report_exclude` in `.periphery.yml` instead). Comment go ABOVE any doc comment, never between doc and declaration. Reuse existing reasons from codebase:
|
||||
|
||||
| Pattern | How recognise | Reason string |
|
||||
|---|---|---|
|
||||
| Rust SDK handle | `TaskHandle`, observation token from SDK, stored after `observe`/`subscribe` call | `required for instance retention in the rust codebase` |
|
||||
| Keep-alive reference | child flow coordinator, service stored so updates keep flowing | `retaining purpose` or `used to avoid deallocation` or `keep alive to keep receiving updates.` |
|
||||
| Cancellable task handle | `@CancellableTask` property, `Task` stored so reassign/nil cancel previous | `auto cancels when reassigned` |
|
||||
| SDK enum exhaustiveness | unused `init` switching over SDK enum | `Unused, but added to detect new cases when updating the SDK.` |
|
||||
| MVVM-C architecture | empty `ViewModelAction` enum from screen template, unused VM protocol — NEVER delete VM protocols, convention keep them | `required for the architecture` (keep the mark even if a superfluous-comment warning appear — it document intent) |
|
||||
| Property wrapper internals | `@propertyWrapper`, `@dynamicMemberLookup` subscript | `property wrappers generate false positives` / `subscript are seen as false positives` |
|
||||
| ObjC selector parameter | parameter only touched by runtime (`displayLink`, `sender` etc.) | `ignore:parameters <name> - required for objc selector` |
|
||||
| Underscored SwiftUI protocol | `_body` from `TextFieldStyle` etc. — called by SwiftUI, members inside resolve transitively | `called by SwiftUI via the TextFieldStyle protocol` |
|
||||
| SDK delegate requirement | method required by rust SDK delegate protocol (e.g. `didRefreshTokens`) | `required by the SDK's delegate protocol` |
|
||||
| Synthesized conformance | field only read via synthesized Hashable/Equatable/Codable (cache keys, diffing models) | `used via the synthesized Hashable conformance` |
|
||||
| Encoded/decoded schema field | Codable field parsed or serialized but not consumed yet | `documents the schema, parsed but not consumed yet` / `part of the encoded payload` |
|
||||
| Release-only code | compiled out in Debug scan (`#if !DEBUG` paths, nightly checks) | `only used in release builds` |
|
||||
| `TestablePreview` conformance | flagged "Redundant protocol conformance" on every preview | NO marker, NO removal — Sourcery read conformance, generate snapshot + a11y tests from it. Remove = tests silently vanish. Skip finding, bulk noise |
|
||||
|
||||
**Unsure if keep-alive?** Property type Task / AnyCancellable / TaskHandle / coordinator / SDK proxy, assigned inside async or stream setup → smell like keep-alive. ASK USER before delete. Show declaration, where assigned, your read on it.
|
||||
|
||||
## Future-use code
|
||||
|
||||
Unused but look intentional — API pair completeness, feature half-landed, "team will need soon". No silent delete, no silent keep. Present to user: what, where, opinion keep-or-delete. User say keep → mark `// periphery:ignore - might be useful to have`.
|
||||
|
||||
## Mock cascade after protocol deletions
|
||||
|
||||
Delete protocol member → Sourcery regenerate mock WITHOUT its `fooReturnValue` / `fooClosure` helpers → hand-written mock `Configuration` inits and test set-up lines still assigning those helpers break the build. Those lines are inert setup for API nothing call — more dead code, not evidence of use. After protocol deletions: grep non-Generated `Mocks/` + `UnitTests/` for `<name>ReturnValue`, `<name>Closure`, direct assignments to deleted members — delete them too. Compiler walk you to the stragglers; that expected, not a false positive.
|
||||
|
||||
Careful with same-name members: one mock file often configure SEVERAL protocols (proxy-level `inviter` vs info-level `inviter`). Strip only lines whose target member actually deleted — the wrong one compile fine (member still exist) but silently un-configure previews; snapshot tests catch it, so run them before calling done.
|
||||
|
||||
## Verify — on the PLAIN project
|
||||
|
||||
Restore the plain configuration BEFORE running tests — snapshots are recorded against plain Element X; Pro branding fail ~8 preview tests falsely:
|
||||
|
||||
```bash
|
||||
git restore project.yml ElementX.xcodeproj/project.pbxproj "ElementX/Resources/AppIcon.icon/Assets/AppIcon.png"
|
||||
xcodegen
|
||||
swift run tools ci unit-tests
|
||||
```
|
||||
|
||||
Sourcery regenerate mocks on build — deleted protocol members clean themselves. All tests pass before call done. Note: the CI helper pipe through `xcbeautify` — if not installed the exit code lie even when tests pass; check the `.xcresult` summary.
|
||||
|
||||
## Purge cache — always
|
||||
|
||||
Periphery DerivedData cache eat tens of GBs. Scan run rare → cache worthless between runs. Last step, no skip:
|
||||
|
||||
```bash
|
||||
rm -rf ~/Library/Caches/com.github.peripheryapp
|
||||
```
|
||||
@@ -0,0 +1,298 @@
|
||||
---
|
||||
name: releasing-healthchat
|
||||
description: Cut a HealthChat TestFlight release of element-x-ios by tagging healthchat-v*. Use when the user asks to release, publish, tag, deploy, ship, or cut a new HealthChat iOS version, asks about the TestFlight or App Store release process, asks about renewing the Apple Distribution signing certificate, or asks about the required GitHub Actions secrets for the release pipeline.
|
||||
---
|
||||
|
||||
# Releasing HealthChat (element-x-ios)
|
||||
|
||||
Releases are driven by git tags of the form `healthchat-v<version>` pushed to
|
||||
`origin`. The tag triggers `.github/workflows/release-healthchat.yml`, which runs
|
||||
on the org's self-hosted macOS runner, builds a signed App Store IPA, and uploads
|
||||
it to **TestFlight only**. No GitHub Release is created.
|
||||
|
||||
**App Store submission is always a manual step** in App Store Connect after a
|
||||
TestFlight build has been validated — see "Promoting to App Store" below.
|
||||
|
||||
**HealthChat ships as an in-place update of the legacy element-ios app.** Same
|
||||
bundle id (`ch.healthchat.element`), same team (`6G37K72W82`), same App Store
|
||||
record. Existing users' keychain and app-group container survive the update. Read
|
||||
"The migration contract" before changing anything that looks like an identifier.
|
||||
|
||||
## Preflight checklist
|
||||
|
||||
Verify each item before tagging. If any fail, fix and commit first.
|
||||
|
||||
- [ ] On branch `release/healthchat` with a clean working tree (`git status`).
|
||||
- [ ] `HEALTHCHAT_CHANGES.md` has a section `## Changes in <new-version> (<date>)`
|
||||
at the top. The pipeline's awk extractor matches this exact heading format;
|
||||
no match ships a fallback placeholder as the TestFlight "What to Test" text.
|
||||
- [ ] The version is **numeric only** — see "Version rules". This is the single
|
||||
most common way to waste 30 minutes here.
|
||||
- [ ] `CHANGES.md` is **not** used for HealthChat releases — upstream's
|
||||
`release-to-github` owns it. Only edit `HEALTHCHAT_CHANGES.md`.
|
||||
|
||||
## Version rules
|
||||
|
||||
**Numeric only. One to three dot-separated integers.** `26.7.4`. Never `26.7.4-rc1`.
|
||||
|
||||
The marketing version lands verbatim in `CFBundleShortVersionString`, and Apple
|
||||
rejects anything that is not at most three period-separated integers. A marker
|
||||
version passes the tag regex and every local gate, then dies at the **upload
|
||||
step, after ~30 minutes of signing**, with an ITMS error indistinguishable from a
|
||||
signing failure.
|
||||
|
||||
The Fastfile guards this in `release_testflight`, before any build or signing work. Do not relax that guard.
|
||||
|
||||
The legacy element-ios skill documents `-rc1` / `-hotfix1` / `-renew-test` tag
|
||||
suffixes. **Do not carry that convention over.** The workflow derives the version
|
||||
by stripping only the `healthchat-v` prefix, so a suffix becomes part of the
|
||||
version and hits the wall above. Put markers in the release notes, and use the
|
||||
`workflow_dispatch` path for test builds.
|
||||
|
||||
`project.yml:56`'s `MARKETING_VERSION` is only a default — **the tag is the source
|
||||
of truth**, and the pipeline injects the version at build time without touching
|
||||
any tracked file. Rebases from upstream will drag `project.yml`'s version around
|
||||
(`automatic-calendar-version.yml` no-ops in this fork); ignore it.
|
||||
|
||||
## Cutting the release (normal path)
|
||||
|
||||
```bash
|
||||
# from release/healthchat, fully committed:
|
||||
git tag healthchat-v<MAJOR>.<MINOR>.<PATCH>
|
||||
git push origin healthchat-v<MAJOR>.<MINOR>.<PATCH>
|
||||
```
|
||||
|
||||
Example: `git tag healthchat-v26.7.4`
|
||||
|
||||
The `healthchat-` prefix keeps these tags clear of upstream's `release/<version>`
|
||||
and `nightly/*` namespaces.
|
||||
|
||||
The workflow triggers automatically. Watch it with `gh run watch` or the Actions
|
||||
UI. The TestFlight build appears in App Store Connect within a few minutes of the
|
||||
run finishing (Apple's processing queue).
|
||||
|
||||
**Build numbers are not encoded in tags.** `generate_build_number` in
|
||||
`fastlane/Fastfile` produces a `YYYYMMDDHHMMSS` timestamp, so ordering is
|
||||
monotonic and a retry never collides with a build number Apple has already seen.
|
||||
|
||||
## Manual / retry path (workflow_dispatch)
|
||||
|
||||
Use when retrying a failed run, testing the pipeline (e.g. cert-renewal
|
||||
verification), or rebuilding without moving tags.
|
||||
|
||||
1. GitHub → Actions → "Release HealthChat" → **Run workflow**.
|
||||
2. Fill in:
|
||||
- **version** (required): e.g. `26.7.99`. Numeric only — same rule as tags.
|
||||
- **release_notes_override** (optional): replaces the `HEALTHCHAT_CHANGES.md`
|
||||
lookup. Use this to mark test builds, since the version cannot carry a marker.
|
||||
3. Run. The IPA is built and uploaded to TestFlight exactly as the tag path would.
|
||||
|
||||
## Release notes
|
||||
|
||||
`HEALTHCHAT_CHANGES.md` is the single source. The workflow extracts the section
|
||||
matching the version being released. Format:
|
||||
|
||||
```markdown
|
||||
## Changes in 26.7.4 (2026-08-01)
|
||||
|
||||
🙌 Improvements
|
||||
|
||||
- Bullet one.
|
||||
- Bullet two.
|
||||
```
|
||||
|
||||
No matching section emits a workflow warning and falls back to
|
||||
`HealthChat <version> — automated build from <ref>.` Prefer fixing the changelog.
|
||||
A **missing file** is a hard error, not a fallback.
|
||||
|
||||
## After the release
|
||||
|
||||
- [ ] Workflow run is green in GitHub Actions.
|
||||
- [ ] Build appears under App Store Connect → TestFlight within ~10 minutes.
|
||||
- [ ] Internal testers are invited (the auto-distribution group may handle this).
|
||||
- [ ] Artifacts (IPA, dSYM, Gym logs) attached to the run, retained 90 days.
|
||||
|
||||
## Never commit the post-lane tree
|
||||
|
||||
A local lane run rewrites `ElementX.xcodeproj/project.pbxproj` — `xcodegen`
|
||||
regenerates it, then `disable_automatic_code_signing` and
|
||||
`update_project_provisioning` write Manual signing and profile UUIDs into it. That
|
||||
file is tracked.
|
||||
|
||||
```bash
|
||||
git checkout -- ElementX.xcodeproj
|
||||
```
|
||||
|
||||
Committing it pins every future build to one machine's profile UUIDs.
|
||||
|
||||
## Required GitHub Actions secrets
|
||||
|
||||
Five. Missing any fails the workflow at the signing step with a clear error.
|
||||
|
||||
| Secret | Source |
|
||||
|---|---|
|
||||
| `HEALTHCHAT_ASC_KEY_ID` | App Store Connect → Users and Access → Integrations → Team Keys (10-char Key ID) |
|
||||
| `HEALTHCHAT_ASC_KEY_ISSUER_ID` | Same page, UUID at the top |
|
||||
| `HEALTHCHAT_ASC_KEY_CONTENT` | `.p8` contents including the `-----BEGIN/END PRIVATE KEY-----` lines |
|
||||
| `HEALTHCHAT_CERTIFICATES_P12` | Base64 distribution cert (`base64 -i healthchat.p12 \| pbcopy`) |
|
||||
| `HEALTHCHAT_CERTIFICATES_P12_PASSWORD` | Password used when exporting the `.p12` |
|
||||
|
||||
**There is no `HEALTHCHAT_KEYCHAIN_PASSWORD`.** The legacy skill lists one and the
|
||||
legacy workflow references it, but it exists at neither repo nor org level and
|
||||
that pipeline runs green anyway — `import-codesign-certs` tolerates its absence.
|
||||
Do not add it back.
|
||||
|
||||
Managing these needs the `snowping` GitHub account; this repo is INTERNAL and
|
||||
invisible to the other account.
|
||||
|
||||
## Self-hosted runner requirements
|
||||
|
||||
Runs on `novaloop-mac-studio-001` — an **org-level** runner (`[self-hosted,
|
||||
macOS, ARM64]`) shared with the legacy fork, not registered per-repo.
|
||||
|
||||
- **One Xcode, globally selected.** A single install at `/Applications/Xcode.app`
|
||||
(**26.6**, build `17F113`, today) — no versioned `Xcode_*.app` siblings.
|
||||
**Nothing pins it.** The Fastfile builds with whatever `xcode-select` points at, so
|
||||
**whatever the runner has is what ships**. Upgrading the runner's Xcode silently
|
||||
changes the compiler every subsequent release is built with — no tag, no diff, no
|
||||
review. Treat that upgrade as a release-affecting change.
|
||||
- **Do not reintroduce a version pin.** Pinning a version the runner lacks sends
|
||||
`xcodes` into a multi-GB authenticated download that no CI secret can satisfy, and
|
||||
the run hangs. Change the runner's Xcode instead.
|
||||
- **Toolchain installed once by hand**, never by CI: `xcodegen swiftlint swiftformat
|
||||
xcbeautify xcresultparser pkl` (`xcresultparser` is not in homebrew-core — only
|
||||
`a7ex/homebrew-formulae/xcresultparser` ships it). `git-lfs`, `xcodes`, `brew` and
|
||||
rbenv `ruby`/`bundle` are already present. There is deliberately **no `brew bundle`
|
||||
step and no `Brewfile`**: this machine is shared with CI, and a per-run
|
||||
`brew update` could upgrade `xcodegen` underneath a running release. CI asserts the
|
||||
tools are present and fails loudly naming the fix; it never provisions.
|
||||
- **No passwordless sudo.** Nothing in CI may call `sudo` — that is why upstream's
|
||||
`setup_github_actions_environment` cannot run here.
|
||||
- **`APPLE_ID`** exported as a machine-level shell variable — the `Appfile` reads
|
||||
it and it is not carried in any secret.
|
||||
- Network access to `developer.apple.com`, `api.appstoreconnect.apple.com`,
|
||||
`github.com`, `rubygems.org`.
|
||||
|
||||
**A release's only Xcode provenance is its own log.** `release-healthchat.yml` runs
|
||||
`xcodebuild -version` (`Record toolchain`) before every build, so each run records the
|
||||
compiler that produced it. Nothing pins that version — upgrading the runner's Xcode
|
||||
silently changes the compiler every subsequent release is built with, and that log
|
||||
line is the only place it shows up. Do not reconstruct a past build's toolchain from
|
||||
the runner: it answers for today, not for the build you are asking about. Read the
|
||||
run's log.
|
||||
|
||||
## Renewing the Apple Distribution certificate (annual)
|
||||
|
||||
Apple Distribution certificates are valid for **exactly 1 year**. There is no
|
||||
longer-lived option. Last renewed **2026-04-07**, so the next is due around
|
||||
**April 2027**. Renew proactively.
|
||||
|
||||
1. **~4 weeks before expiry**, check on the Mac holding the private key:
|
||||
```bash
|
||||
security find-certificate -c "Apple Distribution: E-HealthSolutions GmbH" -p | openssl x509 -noout -enddate
|
||||
```
|
||||
2. **Generate a CSR** on whichever Mac will hold the new private key: Keychain
|
||||
Access → Certificate Assistant → Request a Certificate from a Certificate
|
||||
Authority… → "Saved to disk".
|
||||
3. **Apple Developer portal → Certificates** → **+** → Apple Distribution → upload
|
||||
the CSR → download the `.cer` → double-click to install.
|
||||
4. **Regenerate all 3 App Store provisioning profiles** — they reference the old
|
||||
cert and become invalid:
|
||||
- `HealthChat App Store`
|
||||
- `HealthChat NSE: App Store`
|
||||
- `HealthChat Share Extension: App Store`
|
||||
5. **Verify locally**: archive in Xcode → Distribute App → App Store Connect →
|
||||
Upload. TestFlight accepting the build proves the cert works.
|
||||
6. **Export as `.p12`**: Keychain Access → My Certificates → right-click the new
|
||||
`Apple Distribution: E-HealthSolutions GmbH` → Export → set a password.
|
||||
7. **Re-encode and update secrets**: `base64 -i healthchat-new.p12 | pbcopy`, then
|
||||
replace `HEALTHCHAT_CERTIFICATES_P12` and `HEALTHCHAT_CERTIFICATES_P12_PASSWORD`.
|
||||
8. **Run the workflow** via `workflow_dispatch` with a throwaway numeric version
|
||||
to verify end-to-end. Discard the resulting build.
|
||||
9. **Revoke the old cert** once the new one works in both Xcode and CI.
|
||||
|
||||
**Keep the expired `.p12` archived for 2 weeks** in case a rollback is needed.
|
||||
|
||||
## Promoting a TestFlight build to the App Store
|
||||
|
||||
TestFlight and App Store builds are the same binary. To release a validated one:
|
||||
|
||||
1. App Store Connect → My Apps → HealthChat → **TestFlight** → find the build.
|
||||
2. Confirm it passed internal testing.
|
||||
3. **Distribution** tab → **+** next to "iOS App" → "Prepare for Submission" →
|
||||
fill in metadata → **Add for Review**.
|
||||
4. Wait for review (24–48 hours typical).
|
||||
5. Release manually, or configure automatic release.
|
||||
|
||||
**Deliberately not automated.** Automating submission needs `fastlane/metadata/`
|
||||
plus HealthChat-branded screenshots (neither exists) and a `Deliverfile`
|
||||
refactored for API key auth.
|
||||
|
||||
**Before the first submission**, confirm the install base against the iOS floor:
|
||||
element-x requires **iOS 18.5**, the legacy required 15.1. Every user below 18.5
|
||||
stays on 1.4.1 forever — the App Store will not offer them the update, silently.
|
||||
That floor is upstream's and cannot be lowered.
|
||||
|
||||
## Rollback
|
||||
|
||||
- **Bad build, notes fine** → fix, commit, tag the next patch version. TestFlight
|
||||
supersedes the bad build. (Build numbers are timestamps, so no version games.)
|
||||
- **Bad release notes only** → edit `HEALTHCHAT_CHANGES.md`, then update the
|
||||
TestFlight "What to Test" text in App Store Connect by hand — the pipeline only
|
||||
writes it at upload time.
|
||||
- **Wrong version number** → treat as burned; tag a higher one.
|
||||
- **Rolling back to legacy 1.4.1 loses push** — element-x overwrites the user's
|
||||
sygnal pusher in place on first launch (same app id and pushkey).
|
||||
|
||||
## The migration contract
|
||||
|
||||
**Read this before "cleaning up" anything.** HealthChat replaces the legacy app in
|
||||
place, and these are what stop existing users being silently logged out.
|
||||
|
||||
- **`CLASSIC_APP_*` in `Variants/HealthChat/healthchat.yml` is load-bearing.**
|
||||
`ClassicAppManager` reads the legacy account and crypto-store keys through them.
|
||||
Upstream's defaults in `app.yml` point at Element's own `group.im.vector` /
|
||||
`im.vector.app.*`, which this team cannot provision. **Never restore upstream's
|
||||
values.** Failure is silent — `loadAccounts()` throws, and
|
||||
`AuthenticationService` logs it without telling the user or you.
|
||||
- **The classic keychain group in `ElementX/SupportingFiles/target.yml` must stay
|
||||
in the `keychain-access-groups` array.** Entitlement arrays live there, never in
|
||||
the variant — XcodeGen concatenates arrays on include-merge and only overrides
|
||||
scalars.
|
||||
- **The `.requiresBackup` fall-through in `AuthenticationStartScreenViewModel` is a
|
||||
deliberate upstream divergence.** Upstream sends those accounts to a sheet that
|
||||
tells the user to open Element Classic. That app no longer exists — this update
|
||||
replaced it — and the sheet's button opens a URL scheme nothing registers. A
|
||||
rebase that reverts this reintroduces a dead button.
|
||||
- **Both apps share the `group.ch.healthchat` UserDefaults suite**
|
||||
(`AppSettings.swift:56`). An unqualified `@UserPreference` name lands in a suite
|
||||
the legacy also writes to. Collisions are cheap to introduce.
|
||||
- **`hasSignedInBefore` voids migration testing.** It lives in that shared suite,
|
||||
survives app deletion, and short-circuits `loadAccounts()`. Any device that has
|
||||
run an element-x HealthChat build shows a plain login screen *while migration
|
||||
works perfectly*. Test on a device that has never run an element-x HealthChat
|
||||
build, or clear the `group.ch.healthchat` suite first;
|
||||
`"Already signed in to Element X before, skipping loadAccounts"` in the log
|
||||
means the run is **void, not failed**. A
|
||||
`"Classic App not configured, skipping loadAccounts"` line is the opposite —
|
||||
the `CLASSIC_APP_*` Info.plist keys are missing, `ClassicAppManager` never
|
||||
initialised, and migration is broken for every user. Investigate, do not
|
||||
dismiss.
|
||||
|
||||
## Known branch divergence on rebase
|
||||
|
||||
`fastlane/`, `.github/workflows/release-healthchat.yml`, `HEALTHCHAT_CHANGES.md`,
|
||||
`Gemfile` and this skill are **fork-only** — upstream element-x-ios has
|
||||
no `fastlane/` directory at all. A rebase conflict in any of them means something
|
||||
upstream has started to overlap; keep `--ours` and investigate.
|
||||
|
||||
What does need watching:
|
||||
|
||||
- **`project.yml`'s `MARKETING_VERSION`** — `automatic-calendar-version.yml` and
|
||||
`post-release.yml` are gated on `github.repository == 'element-hq/element-x-ios'`
|
||||
and silently no-op here, so rebases drag upstream's version along. Harmless: the
|
||||
tag is the source of truth.
|
||||
- **`AuthenticationStartScreenViewModel.login(classicAppAccount:)`** — the only
|
||||
upstream logic file this fork edits. See "The migration contract".
|
||||
- **`app.yml`'s `CLASSIC_APP_*`** — upstream owns these and they stay pointed at
|
||||
Element's ids. The variant overrides them. Do not "fix" `app.yml`.
|
||||
@@ -1,5 +1,5 @@
|
||||
UITests/Sources/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text
|
||||
DevelopmentAssets/Media/** filter=lfs diff=lfs merge=lfs -text
|
||||
Components/DevelopmentAssets/Media/** filter=lfs diff=lfs merge=lfs -text
|
||||
UnitTests/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text
|
||||
PreviewTests/Sources/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text
|
||||
compound-ios/Tests/CompoundTests/__Snapshots__/** filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -6,7 +6,7 @@ git lfs post-checkout "$@"
|
||||
export PATH="$PATH:/opt/homebrew/bin"
|
||||
|
||||
# ignores updates of 'Secrets.swift' to avoid pushing sensitive data by mistake
|
||||
git update-index --assume-unchanged Secrets/Secrets.swift
|
||||
git update-index --assume-unchanged Components/Secrets/Secrets.swift
|
||||
|
||||
LOCAL_SCRIPT_PATH="$HOME/.local/elementx_local_dev/post-checkout"
|
||||
if [ -x "$LOCAL_SCRIPT_PATH" ]; then
|
||||
|
||||
@@ -28,6 +28,10 @@ changelog:
|
||||
labels:
|
||||
- pr-wip
|
||||
|
||||
- title: 💼 Element Pro
|
||||
labels:
|
||||
- pr-element-pro
|
||||
|
||||
- title: Others
|
||||
labels:
|
||||
- pr-misc
|
||||
|
||||
@@ -11,6 +11,11 @@ permissions: {}
|
||||
jobs:
|
||||
tests:
|
||||
name: Tests
|
||||
# HealthChat: skip in this fork. It runs nightly Mon-Fri and fails at checkout, because
|
||||
# permissions: {} leaves a metadata-only token that cannot read an INTERNAL repo. Gated rather
|
||||
# than deleted so the file keeps rebasing cleanly. Run locally with
|
||||
# `swift run tools ci accessibility-tests`.
|
||||
if: github.repository == 'element-hq/element-x-ios'
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 150
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
if: github.repository == 'element-hq/element-x-ios'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@ permissions: {}
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
# HealthChat: skip in this fork. It only triggers on changes under compound-ios/, which this fork
|
||||
# has never edited, so it would only ever fire on an upstream sync and go red at checkout —
|
||||
# permissions: {} leaves a metadata-only token that cannot read an INTERNAL repo. Gated rather
|
||||
# than deleted so the file keeps rebasing cleanly.
|
||||
if: github.repository == 'element-hq/element-x-ios'
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 15
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
name: Danger CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, synchronize, opened, reopened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Danger
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
concurrency:
|
||||
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
|
||||
group: ${{ format('danger-ci-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Danger
|
||||
uses: danger/swift@434d7c25f3b02d490a340b23f5e78dd15a5670bc # 3.22.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -11,6 +11,13 @@ permissions: {}
|
||||
jobs:
|
||||
integration_tests:
|
||||
name: Integration Tests
|
||||
# HealthChat: skip in this fork. It runs nightly Mon-Fri and fails at checkout, and it is the one
|
||||
# job that still calls out to Element's Codecov — its upload guard is true on schedule events, so
|
||||
# it POSTs slug/branch/SHA every night with continue-on-error hiding the failure. Fixing it would
|
||||
# need a real healthchat.ch test account and three new secrets, and would still not work:
|
||||
# HealthChatAppSettingsHook sets allowOtherAccountProviders: false, hiding the change-server UI
|
||||
# these tests drive. Gated rather than deleted so the file keeps rebasing cleanly.
|
||||
if: github.repository == 'element-hq/element-x-ios'
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 45
|
||||
|
||||
@@ -20,7 +27,7 @@ jobs:
|
||||
cancel-in-progress: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -40,7 +47,9 @@ jobs:
|
||||
if: failure()
|
||||
with:
|
||||
name: Results
|
||||
path: test_output/IntegrationTests.xcresult.zip
|
||||
path: |
|
||||
test_output/IntegrationTests.xcresult.zip
|
||||
test_output/console*
|
||||
retention-days: 7
|
||||
if-no-files-found: ignore
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
name: PR Checks
|
||||
|
||||
on:
|
||||
# Privilege escalation necessary to comment on pull requests from forks.
|
||||
# 🚨 We must not check out or execute any code here, and be careful around use of user-controlled inputs.
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||
types: [opened, reopened, edited, synchronize, labeled, unlabeled]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
pr-checks:
|
||||
name: PR Checks
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
|
||||
concurrency:
|
||||
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
|
||||
group: ${{ format('pr-checks-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Check the pull request
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const body = pr.body ?? "";
|
||||
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number
|
||||
});
|
||||
const paths = files.map(file => file.filename.toLowerCase());
|
||||
|
||||
const warnings = [];
|
||||
const failures = [];
|
||||
|
||||
if (pr.additions > 1000) {
|
||||
warnings.push("This pull request seems relatively large. Please consider splitting it into multiple smaller ones.");
|
||||
}
|
||||
|
||||
if (!body) {
|
||||
warnings.push("Please provide a description for this PR.");
|
||||
}
|
||||
|
||||
if (paths.some(path => path.includes("/view")) && !body.includes("user-attachments")) {
|
||||
warnings.push("You seem to have made changes to views. Please consider adding screenshots.");
|
||||
}
|
||||
|
||||
if (paths.some(path => path.includes(".xcassets") && path.endsWith(".png"))) {
|
||||
warnings.push("You seem to have made changes to some resource images. Please consider using an SVG or PDF.");
|
||||
}
|
||||
|
||||
if (pr.title.endsWith("…") || /^(Fixes|Fix) #\d+/.test(pr.title) || /^\w+\(\w+\):/.test(pr.title)) {
|
||||
failures.push("Please provide a complete title that can be used as a changelog entry.");
|
||||
}
|
||||
|
||||
if (pr.labels.filter(label => label.name.startsWith("pr-")).length !== 1) {
|
||||
failures.push("Please add a `pr-` label to categorise the changelog entry.");
|
||||
}
|
||||
|
||||
warnings.forEach(warning => core.warning(warning));
|
||||
failures.forEach(failure => core.error(failure));
|
||||
|
||||
// Keep a single comment up to date rather than adding a new one on every run.
|
||||
const marker = "<!-- pr-checks -->";
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number
|
||||
});
|
||||
const existing = comments.find(comment => comment.body.includes(marker));
|
||||
|
||||
if (warnings.length || failures.length) {
|
||||
const lines = [...failures.map(failure => `- ❌ ${failure}`),
|
||||
...warnings.map(warning => `- ⚠️ ${warning}`)];
|
||||
const commentBody = `${marker}\n### PR Checks\n\n${lines.join("\n")}`;
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id,
|
||||
body: commentBody
|
||||
});
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
body: commentBody
|
||||
});
|
||||
}
|
||||
} else if (existing) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: existing.id
|
||||
});
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
core.setFailed(`${failures.length} check(s) failed, see the comment on the pull request.`);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
name: Release HealthChat
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'healthchat-v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Marketing version (e.g. 26.7.4). Must be 1-3 dot-separated integers — Apple rejects anything else.'
|
||||
required: true
|
||||
type: string
|
||||
release_notes_override:
|
||||
description: 'Optional release notes. If empty, falls back to the HEALTHCHAT_CHANGES.md section for the given version.'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
# Minimum needed for actions/checkout to clone an internal repo. The workflow does not push,
|
||||
# comment, or create releases — everything else authenticates with the ASC key and .p12.
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-healthchat-${{ github.event.inputs.version || github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build & upload to TestFlight
|
||||
runs-on: [self-hosted, macOS]
|
||||
timeout-minutes: 90
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# This repo's only submodule is element-hq's private Enterprise repo, which Novaloop
|
||||
# cannot clone. Checking out submodules fails the job on its very first step.
|
||||
submodules: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Record toolchain
|
||||
# HealthChat: nothing pins Xcode (the runner carries a single globally-selected one), so this
|
||||
# log line is the only record of which compiler produced a given TestFlight build.
|
||||
run: xcodebuild -version
|
||||
|
||||
- name: Resolve marketing version
|
||||
id: version
|
||||
env:
|
||||
# Read via env, never `${{ }}` inside the script: the input is attacker-controlled text
|
||||
# that GitHub would splice into the shell before the regex below could reject it.
|
||||
INPUT_VERSION: ${{ github.event.inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||
VERSION="$INPUT_VERSION"
|
||||
else
|
||||
VERSION="${GITHUB_REF_NAME#healthchat-v}"
|
||||
fi
|
||||
# Apple caps CFBundleShortVersionString at three integers. The Fastfile guards this too,
|
||||
# but failing here costs seconds instead of a signing run.
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]]; then
|
||||
echo "::error::Invalid marketing version '$VERSION' — expected 1-3 dot-separated integers, e.g. 26.7.4"
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Resolved marketing version: $VERSION"
|
||||
|
||||
- name: Extract release notes
|
||||
id: notes
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
OVERRIDE: ${{ github.event.inputs.release_notes_override }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ -n "$OVERRIDE" ]]; then
|
||||
NOTES="$OVERRIDE"
|
||||
echo "Using release notes from the workflow input"
|
||||
elif [[ ! -f HEALTHCHAT_CHANGES.md ]]; then
|
||||
echo "::error::HEALTHCHAT_CHANGES.md is missing — it is the release-notes source"
|
||||
exit 1
|
||||
else
|
||||
NOTES=$(awk -v ver="$VERSION" '
|
||||
/^## Changes in / {
|
||||
if (found) { exit }
|
||||
if ($0 ~ "## Changes in " ver "( |$|\\()") { found = 1; next }
|
||||
}
|
||||
found { print }
|
||||
' HEALTHCHAT_CHANGES.md | sed '/^[[:space:]]*$/{N;/^\n$/D;}')
|
||||
if [[ -z "$NOTES" ]]; then
|
||||
NOTES="HealthChat ${VERSION} — automated build from ${GITHUB_REF_NAME}."
|
||||
echo "::warning::No HEALTHCHAT_CHANGES.md section found for $VERSION — using fallback"
|
||||
fi
|
||||
fi
|
||||
{
|
||||
echo 'notes<<NOTES_EOF'
|
||||
echo "$NOTES"
|
||||
echo 'NOTES_EOF'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Bundle install
|
||||
run: |
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
|
||||
- name: Import signing certificate
|
||||
uses: apple-actions/import-codesign-certs@b610f78488812c1e56b20e6df63ec42d833f2d14 # v6.0.0
|
||||
with:
|
||||
p12-file-base64: ${{ secrets.HEALTHCHAT_CERTIFICATES_P12 }}
|
||||
p12-password: ${{ secrets.HEALTHCHAT_CERTIFICATES_P12_PASSWORD }}
|
||||
|
||||
- name: Build & upload to TestFlight
|
||||
run: bundle exec fastlane release_testflight
|
||||
env:
|
||||
# Passed via env rather than lane options so multi-line notes containing quotes,
|
||||
# $, or backticks survive without shell escaping.
|
||||
HEALTHCHAT_MARKETING_VERSION: ${{ steps.version.outputs.version }}
|
||||
HEALTHCHAT_RELEASE_NOTES: ${{ steps.notes.outputs.notes }}
|
||||
APPSTORECONNECT_KEY_ID: ${{ secrets.HEALTHCHAT_ASC_KEY_ID }}
|
||||
APPSTORECONNECT_KEY_ISSUER_ID: ${{ secrets.HEALTHCHAT_ASC_KEY_ISSUER_ID }}
|
||||
APPSTORECONNECT_KEY_CONTENT: ${{ secrets.HEALTHCHAT_ASC_KEY_CONTENT }}
|
||||
TEAM_ID: '6G37K72W82'
|
||||
|
||||
- name: Upload artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: healthchat-${{ steps.version.outputs.version }}
|
||||
path: |
|
||||
out/ElementX.ipa
|
||||
out/ElementX.app.dSYM.zip
|
||||
build/Logs/Gym/*.log
|
||||
retention-days: 90
|
||||
if-no-files-found: warn
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.head_ref }}
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
|
||||
@@ -13,17 +13,21 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11
|
||||
with:
|
||||
only-labels: "X-Needs-Info"
|
||||
labels-to-remove-when-unstale: "X-Needs-Info"
|
||||
|
||||
# Issues
|
||||
only-issue-labels: "X-Needs-Info"
|
||||
days-before-issue-stale: 30
|
||||
days-before-issue-close: 7
|
||||
days-before-pr-stale: 30
|
||||
days-before-pr-close: 7
|
||||
stale-pr-label: "stale"
|
||||
stale-pr-message: "This pull request has seen no activity for the past 30 days and will now be marked as stale. Please update it or leave a comment within the next 7 days to keep it open."
|
||||
close-pr-message: "This pull request is being closed due to inactivity."
|
||||
stale-issue-label: "stale"
|
||||
labels-to-remove-when-unstale: "X-Needs-Info"
|
||||
stale-issue-label: "Stale"
|
||||
stale-issue-message: "This issue has been awaiting further information for the past 30 days so will now be marked as stale. Please provide the requested information within the next 7 days to keep it open."
|
||||
close-issue-message: "This issue is being closed due to inactivity after further information was requested."
|
||||
|
||||
# Pull requests
|
||||
days-before-pr-stale: 30
|
||||
days-before-pr-close: 7
|
||||
stale-pr-label: "Stale"
|
||||
stale-pr-message: "This pull request has seen no activity for the past 30 days and will now be marked as stale. Please update it or leave a comment within the next 7 days to keep it open."
|
||||
close-pr-message: "This pull request is being closed due to inactivity."
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
if: github.repository == 'element-hq/element-x-ios'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -15,6 +15,12 @@ permissions: {}
|
||||
jobs:
|
||||
tests:
|
||||
name: Tests
|
||||
# HealthChat: skip in this fork. It runs nightly Mon-Fri and fails at checkout, because
|
||||
# permissions: {} leaves a metadata-only token that cannot read an INTERNAL repo. Fixing the
|
||||
# permission would not help: the rebrand changed the start screen without re-recording any
|
||||
# UITests snapshot, so it would fail on content instead. Gated rather than deleted so the file
|
||||
# keeps rebasing cleanly. Run locally with `swift run tools ci ui-tests`.
|
||||
if: github.repository == 'element-hq/element-x-ios'
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 90
|
||||
strategy:
|
||||
|
||||
@@ -17,7 +17,14 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
|
||||
# Skip in forks
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
# HealthChat: this job can never pass in this fork — it needs
|
||||
# secrets.ELEMENT_BOT_TOKEN (Element's bot, absent here) and recursively
|
||||
# checks out the private element-hq/element-ios-enterprise submodule, which
|
||||
# Novaloop cannot obtain. The guard below only asks whether the PR comes
|
||||
# FROM a fork, not whether this repo IS one, so it does not skip us. Gate on
|
||||
# the upstream repo as post-release.yml does. Gated rather than deleted so
|
||||
# the file keeps rebasing cleanly against upstream.
|
||||
if: ${{ github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
|
||||
concurrency:
|
||||
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
|
||||
@@ -34,12 +41,27 @@ jobs:
|
||||
- name: Setup environment
|
||||
run: source ci_scripts/ci_common.sh && setup_github_actions_environment
|
||||
|
||||
- name: Restore compilation cache
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/Library/Developer/Xcode/CompilationCache.noindex
|
||||
key: compilation-cache-unit-tests-enterprise-${{ github.sha }}
|
||||
restore-keys: compilation-cache-unit-tests-enterprise-
|
||||
|
||||
- name: Configure Enterprise
|
||||
run: swift run -q pipeline configure-element-pro
|
||||
|
||||
- name: Run tests
|
||||
run: swift run -q tools ci unit-tests --skip-previews
|
||||
|
||||
- name: Save compilation cache
|
||||
# Only develop populates the cache, all of the other runs simply read from it.
|
||||
if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/Library/Developer/Xcode/CompilationCache.noindex
|
||||
key: compilation-cache-unit-tests-enterprise-${{ github.sha }}
|
||||
|
||||
- name: Archive artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
# We only care about artefacts if the tests fail
|
||||
|
||||
@@ -13,24 +13,93 @@ permissions: {}
|
||||
jobs:
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: macos-26
|
||||
# HealthChat: GitHub-hosted macos-26 could not finish this job inside
|
||||
# timeout-minutes: 60 (two attempts: 51m42s, then killed at 60m44s), and macOS
|
||||
# minutes bill at 10x. novaloop-mac-studio-001 is the org's Mac Studio, already
|
||||
# used by the release pipeline; its labels are [self-hosted, macOS, ARM64].
|
||||
runs-on: [self-hosted, macOS]
|
||||
# Upstream's 60 is kept: with previews skipped (see Run tests) the build plus unit tests measured
|
||||
# ~16 min here. A run approaching this budget is a signal, not normal — the earlier 60m timeouts
|
||||
# were preview tests, not the build.
|
||||
timeout-minutes: 60
|
||||
|
||||
permissions:
|
||||
contents: read # HealthChat: this fork is INTERNAL, so a metadata-only token 404s on checkout (and on git lfs pull). Upstream is public, hence permissions: {} there.
|
||||
|
||||
concurrency:
|
||||
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
|
||||
group: ${{ format('unit-tests-{0}', github.ref) }}
|
||||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
# HealthChat: upstream passes persist-credentials: false here, which the
|
||||
# composite forwards to actions/checkout — stripping the auth header before
|
||||
# its separate "Git LFS Pull" step runs. Upstream can afford that because
|
||||
# its repo is public and git lfs pull works anonymously; this fork is
|
||||
# INTERNAL, so the LFS batch endpoint would 404. Keep the composite's own
|
||||
# default (true) so the pull is authenticated with GITHUB_TOKEN.
|
||||
- uses: nschloe/action-cached-lfs-checkout@385a8ecc719e50b8c71af6ab01a624b486b7c3bc # v1.2.5
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup environment
|
||||
run: source ci_scripts/ci_common.sh && setup_github_actions_environment
|
||||
# HealthChat: verify only — never provision. Upstream's
|
||||
# setup_github_actions_environment cannot run on this runner: it calls
|
||||
# `sudo xcode-select -s /Applications/Xcode_26.5.0.app`, but the runner has no
|
||||
# passwordless sudo and no such path (only /Applications/Xcode.app exists), and
|
||||
# it calls `brew update && brew install ...` — novaloop-mac-studio-001 is the
|
||||
# shared release machine, so a per-run `brew update` could upgrade xcodegen
|
||||
# underneath the release pipeline. These tools are installed once by hand; CI
|
||||
# only asserts they are present and fails loudly, naming the fix, if not.
|
||||
- name: Verify environment
|
||||
run: |
|
||||
set -eu
|
||||
missing=""
|
||||
formulae=""
|
||||
for tool in xcodegen swiftlint swiftformat xcbeautify xcresultparser pkl git-lfs; do
|
||||
if ! command -v "$tool" >/dev/null 2>&1; then
|
||||
missing="$missing $tool"
|
||||
case "$tool" in
|
||||
# Not in homebrew-core; only this tap ships it.
|
||||
xcresultparser) formulae="$formulae a7ex/homebrew-formulae/xcresultparser" ;;
|
||||
*) formulae="$formulae $tool" ;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
if [ -n "$missing" ]; then
|
||||
echo "::error::Missing on self-hosted runner:${missing}. Install by hand on novaloop-mac-studio-001, then re-run: brew install${formulae}"
|
||||
exit 1
|
||||
fi
|
||||
# The Xcode version is no longer pinned anywhere, so this is the only record
|
||||
# of which Xcode built a given run.
|
||||
xcodebuild -version
|
||||
|
||||
- name: Restore compilation cache
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/Library/Developer/Xcode/CompilationCache.noindex
|
||||
key: compilation-cache-unit-tests-${{ github.sha }}
|
||||
restore-keys: compilation-cache-unit-tests-
|
||||
|
||||
- name: Run tests
|
||||
run: swift run -q tools ci unit-tests
|
||||
# HealthChat: previews are skipped. PreviewTests renders a snapshot for every preview in the
|
||||
# app and was the entire reason this job could not fit in an hour — it never once finished,
|
||||
# on either runner. Unit tests alone take ~16 min here. Upstream skips them the same way in
|
||||
# unit-tests-enterprise.yml. Cost: preview snapshot regressions are not caught in CI, so
|
||||
# re-record and check them locally when changing UI.
|
||||
env:
|
||||
# HealthChat: VoiceMessageCacheTests cannot write its URLFileProtection.complete cache file on
|
||||
# this runner's simulator (NSCocoaErrorDomain 513) — a runner-environment issue, not a code bug:
|
||||
# the suite passes locally and upstream, and neither recreating+booting the device nor clearing
|
||||
# state fixes it. Skip it here only, via -skip-testing. Drop this once the runner can write
|
||||
# data-protected files again.
|
||||
CI_SKIP_TESTS: VoiceMessageCacheTests
|
||||
run: swift run -q tools ci unit-tests --skip-previews
|
||||
|
||||
- name: Save compilation cache
|
||||
# Only develop populates the cache, all of the other runs simply read from it.
|
||||
if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/Library/Developer/Xcode/CompilationCache.noindex
|
||||
key: compilation-cache-unit-tests-${{ github.sha }}
|
||||
|
||||
- name: Archive artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
@@ -44,7 +113,11 @@ jobs:
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
# Skip if not successful and in forks
|
||||
if: ${{ success() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
# HealthChat: the existing guard only asks whether the PR comes FROM a
|
||||
# fork, not whether this repo IS one, so it is true here and the step
|
||||
# runs without a CODECOV_TOKEN — which fails the job via
|
||||
# fail_ci_if_error. Gate on the upstream repo as post-release.yml does.
|
||||
if: ${{ success() && github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
report_type: coverage
|
||||
@@ -56,7 +129,9 @@ jobs:
|
||||
|
||||
- name: Upload test results to Codecov
|
||||
# Skip if cancelled and in forks
|
||||
if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
# HealthChat: gated on the upstream repo for the same reason as the
|
||||
# coverage step above — no CODECOV_TOKEN exists in this fork.
|
||||
if: ${{ !cancelled() && github.repository == 'element-hq/element-x-ios' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
|
||||
@@ -14,11 +14,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
|
||||
contents: read # HealthChat: this fork is INTERNAL, so a metadata-only token 404s on checkout. Upstream is public, hence permissions: {} there.
|
||||
actions: read # HealthChat: upload-sarif additionally needs this on private/internal repos; public upstream does not.
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor 🌈
|
||||
uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
|
||||
uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3
|
||||
|
||||
@@ -47,5 +47,12 @@ build
|
||||
test_output
|
||||
|
||||
# Only commit Pkl packages (there may be other caches in here).
|
||||
Secrets/vendor/*
|
||||
!Secrets/vendor/package-2
|
||||
Components/Secrets/vendor/*
|
||||
!Components/Secrets/vendor/package-2
|
||||
|
||||
# fastlane release outputs
|
||||
out/
|
||||
provisioning_profiles/
|
||||
# Regenerated by every fastlane invocation, including on the runner. Ignored rather than
|
||||
# committed, matching element-ios.
|
||||
fastlane/README.md
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
project: ElementX.xcodeproj
|
||||
# Periphery scans all targets built by these schemes. The ElementX scheme also
|
||||
# builds NSE, ShareExtension and MatrixRustSDKMocks as dependencies. Always run
|
||||
# the scan with the project configured for the Pro variant so that all of the
|
||||
# sources are analysed (see the periphery-cleanup skill).
|
||||
schemes:
|
||||
- ElementX
|
||||
- UnitTests
|
||||
- PreviewTests
|
||||
- UITests
|
||||
- IntegrationTests
|
||||
targets:
|
||||
- ElementX
|
||||
- NSE
|
||||
- ShareExtension
|
||||
- UnitTests
|
||||
- PreviewTests
|
||||
- UITests
|
||||
- IntegrationTests
|
||||
- AccessibilityTests
|
||||
# Compound is excluded: its Inspector app and package tests aren't part of any
|
||||
# scheme in this project, so usage from them is invisible to the analysis.
|
||||
index_exclude:
|
||||
- "**/*?.build/**/*"
|
||||
- "**/SourcePackages/checkouts/**"
|
||||
- "compound-ios/**"
|
||||
report_exclude:
|
||||
- ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
|
||||
- ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift
|
||||
- "**/Generated/**"
|
||||
# Hand-written mocks: configuration surface is deliberately broader than any test uses.
|
||||
- "**/Mocks/**"
|
||||
- "ElementX/Sources/Services/AppLock/LAContextMock.swift"
|
||||
# Pure payload schema, fields exist to be encoded.
|
||||
- "ElementX/Sources/Services/Notification/Manager/APNSPayload.swift"
|
||||
# Backports: everything gated on #available, cleaned up with each OS drop.
|
||||
- "ElementX/Sources/Other/SwiftUI/Backports.swift"
|
||||
retain_swift_ui_previews: true
|
||||
# Protocol signatures are contracts (hooks, delegates) — parameters unused in
|
||||
# every current conformance are still part of the interface's design.
|
||||
retain_unused_protocol_func_params: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
--swiftversion 5.6
|
||||
|
||||
--exclude **/Sources/**/Generated,vendor,**/Package.swift,Secrets/**
|
||||
--exclude **/Sources/**/Generated,vendor,**/Package.swift,Components/Secrets/**
|
||||
|
||||
--disable wrapMultiLineStatementBraces
|
||||
--disable hoistPatternLet
|
||||
|
||||
@@ -3,6 +3,7 @@ disabled_rules:
|
||||
- unused_setter_value
|
||||
- redundant_discardable_let
|
||||
- identifier_name
|
||||
- void_function_in_ternary
|
||||
|
||||
opt_in_rules:
|
||||
- force_unwrapping
|
||||
@@ -17,7 +18,7 @@ included:
|
||||
excluded:
|
||||
- "*/Sources/Generated/*"
|
||||
- Tools/Sources # Our command-line tools use a lot of print commands.
|
||||
- Secrets
|
||||
- Components/Secrets
|
||||
- compound-ios/Inspector
|
||||
|
||||
line_length:
|
||||
|
||||
@@ -14,6 +14,9 @@ PRs must follow rules. Prefer Xcode MCP tools over terminal commands.
|
||||
- **SwiftLint** (.swiftlint.yml) + **SwiftFormat** (.swiftformat) enforce style on build. Warnings show in Xcode. Build before **XcodeRefreshCodeIssuesInFile**.
|
||||
- Follow [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/) everywhere, Rust SDK wrappers too (`ID` not `Id`, `URL` not `Url`, `configuration` not `config` or `cfg`).
|
||||
- File headers live in `IDETemplateMacros.plist`.
|
||||
- **Import `SwiftUI`, not `UIKit`** — need `UIPasteboard` etc, `SwiftUI` bring it. `SwiftUI` re-export `Foundation` too, so no `import Foundation` beside it.
|
||||
- **Clean Code** (Robert C. Martin) Newspaper order: headline first, detail down. Stepdown rule: caller above callee, calls point down. Tests: test cases first, helpers below. Swift idiom always win on clash.
|
||||
- **Member order in type**: properties (stored + computed, public + private) → `init` → functions. `View` types: properties → `init` → views (`body` first) → functions.
|
||||
|
||||
### Comments
|
||||
|
||||
@@ -52,6 +55,7 @@ PRs must follow rules. Prefer Xcode MCP tools over terminal commands.
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
- New branch from `develop` must not track `develop`.
|
||||
- Sentence-style titles (no conventional commits).
|
||||
- Exactly one `pr-` label (see `.github/release.yml`).
|
||||
- Title = changelog entry — descriptive, no "Fixes #…".
|
||||
@@ -63,6 +67,14 @@ PRs must follow rules. Prefer Xcode MCP tools over terminal commands.
|
||||
|
||||
---
|
||||
|
||||
## Releasing
|
||||
|
||||
HealthChat ship via tag `healthchat-v*` → TestFlight. Full runbook in
|
||||
`.agents/skills/releasing-healthchat/SKILL.md`. Read skill before release, cert
|
||||
renewal, or touching `fastlane/`. No duplicate runbook here.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Build System
|
||||
@@ -378,7 +390,7 @@ Same pattern for publishers.
|
||||
| `app.yml` | App-level XcodeGen config |
|
||||
| `.swiftlint.yml` | SwiftLint rules |
|
||||
| `.swiftformat` | SwiftFormat rules |
|
||||
| `Dangerfile.swift` | Danger PR checks |
|
||||
| `.github/workflows/pr-checks.yml` | PR metadata checks (title, labels, size, screenshots) |
|
||||
| `Package.swift` | SPM manifest (Tools CLI) |
|
||||
| `localazy.json` | Localazy translation config |
|
||||
| `codecov.yml` | Codecov config |
|
||||
|
||||
@@ -53,6 +53,9 @@ final class AccessibilityTests: XCTestCase {
|
||||
private func performAccessibilityAuditForPreview(named name: String) {
|
||||
// Alows us to log the name of the preview that is being tested
|
||||
XCTContext.runActivity(named: name) { _ in
|
||||
// Previews that focus a text field bring up the system keyboard, which we can't fix.
|
||||
let keyboardFrame = app.keyboards.firstMatch.exists ? app.keyboards.firstMatch.frame : .null
|
||||
|
||||
do {
|
||||
// We have removed `textClipped` and `contrast` for now
|
||||
try app.performAccessibilityAudit(for: [.dynamicType, .elementDetection, .hitRegion, .sufficientElementDescription, .trait]) { issue in
|
||||
@@ -66,6 +69,11 @@ final class AccessibilityTests: XCTestCase {
|
||||
return true
|
||||
}
|
||||
|
||||
// The system keyboard's own elements aren't ours to fix
|
||||
if keyboardFrame.contains(element.frame) {
|
||||
return true
|
||||
}
|
||||
|
||||
// We are fine with elements that only partially support dynamic types
|
||||
guard issue.compactDescription != Self.partiallyUnsupportedDynamicTypeMessage else {
|
||||
return true
|
||||
@@ -98,7 +106,6 @@ final class AccessibilityTests: XCTestCase {
|
||||
|
||||
private static let partiallyUnsupportedDynamicTypeMessage = "Dynamic Type font sizes are partially unsupported"
|
||||
private static let notHumanReadableMessage = "Label not human-readable"
|
||||
private static let elementHasNoDescription = "Element has no description"
|
||||
|
||||
/// Use this array to filter add specific filters to ignore specific issues for certain elements
|
||||
private static let ignoredA11yIdentifiers: [String: [FilterType]] = [
|
||||
@@ -108,11 +115,7 @@ final class AccessibilityTests: XCTestCase {
|
||||
/// Use this array to filter add specific filters to ignore specific tests
|
||||
private static let ignoredA11yTest: [String: [FilterType]] = [
|
||||
// It's an image rendering test doesn't need to have descriptions
|
||||
"RoomAvatarImage_Previews-0": [.auditType(.sufficientElementDescription)],
|
||||
// Empty container slots of the principal search toolbar, system chrome with nothing to describe
|
||||
"SearchScreen_Previews-Empty": [.emptyContainerDescription],
|
||||
"SearchScreen_Previews-No results": [.emptyContainerDescription],
|
||||
"SearchScreen_Previews-Loaded": [.emptyContainerDescription]
|
||||
"RoomAvatarImage_Previews-0": [.auditType(.sufficientElementDescription)]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -121,8 +124,6 @@ private enum FilterType {
|
||||
case compactDescription(String)
|
||||
/// Filter by the type of the issue
|
||||
case auditType(XCUIAccessibilityAuditType)
|
||||
/// Filter "no description" issues on empty container elements (label + identifier empty), e.g. system toolbar slots
|
||||
case emptyContainerDescription
|
||||
}
|
||||
|
||||
private extension Array where Element == FilterType {
|
||||
@@ -134,14 +135,6 @@ private extension Array where Element == FilterType {
|
||||
return true
|
||||
case .compactDescription(issue.compactDescription):
|
||||
return true
|
||||
case .emptyContainerDescription:
|
||||
if issue.auditType == .sufficientElementDescription,
|
||||
let element = issue.element,
|
||||
element.elementType == .other,
|
||||
element.label.isEmpty,
|
||||
element.identifier.isEmpty {
|
||||
return true
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
|
||||
extension AccessibilityTests {
|
||||
|
||||
func testActiveCallTimelineItemView() async throws {
|
||||
try await performAccessibilityAudit(named: "ActiveCallTimelineItemView_Previews")
|
||||
}
|
||||
|
||||
func testAdvancedSettingsScreen() async throws {
|
||||
try await performAccessibilityAudit(named: "AdvancedSettingsScreen_Previews")
|
||||
}
|
||||
@@ -55,6 +59,10 @@ extension AccessibilityTests {
|
||||
try await performAccessibilityAudit(named: "AvatarHeaderView_Previews")
|
||||
}
|
||||
|
||||
func testAvatarSettingsButtonLabel() async throws {
|
||||
try await performAccessibilityAudit(named: "AvatarSettingsButtonLabel_Previews")
|
||||
}
|
||||
|
||||
func testBadgeLabel() async throws {
|
||||
try await performAccessibilityAudit(named: "BadgeLabel_Previews")
|
||||
}
|
||||
@@ -183,6 +191,26 @@ extension AccessibilityTests {
|
||||
try await performAccessibilityAudit(named: "FullscreenDialog_Previews")
|
||||
}
|
||||
|
||||
func testGalleryRoomTimelineView() async throws {
|
||||
try await performAccessibilityAudit(named: "GalleryRoomTimelineView_Previews")
|
||||
}
|
||||
|
||||
func testHealthChatExtendedProfileSection() async throws {
|
||||
try await performAccessibilityAudit(named: "HealthChatExtendedProfileSection_Previews")
|
||||
}
|
||||
|
||||
func testHealthChatInviteMemberScreen() async throws {
|
||||
try await performAccessibilityAudit(named: "HealthChatInviteMemberScreen_Previews")
|
||||
}
|
||||
|
||||
func testHealthChatMemberDirectoryScreen() async throws {
|
||||
try await performAccessibilityAudit(named: "HealthChatMemberDirectoryScreen_Previews")
|
||||
}
|
||||
|
||||
func testHealthChatProfileEditScreen() async throws {
|
||||
try await performAccessibilityAudit(named: "HealthChatProfileEditScreen_Previews")
|
||||
}
|
||||
|
||||
func testHighlightedTimelineItemModifier() async throws {
|
||||
try await performAccessibilityAudit(named: "HighlightedTimelineItemModifier_Previews")
|
||||
}
|
||||
@@ -479,6 +507,10 @@ extension AccessibilityTests {
|
||||
try await performAccessibilityAudit(named: "ReadReceiptsSummaryView_Previews")
|
||||
}
|
||||
|
||||
func testRedactConfirmationView() async throws {
|
||||
try await performAccessibilityAudit(named: "RedactConfirmationView_Previews")
|
||||
}
|
||||
|
||||
func testRedactedRoomTimelineView() async throws {
|
||||
try await performAccessibilityAudit(named: "RedactedRoomTimelineView_Previews")
|
||||
}
|
||||
@@ -647,10 +679,6 @@ extension AccessibilityTests {
|
||||
try await performAccessibilityAudit(named: "SeparatorRoomTimelineView_Previews")
|
||||
}
|
||||
|
||||
func testServerConfirmationScreen() async throws {
|
||||
try await performAccessibilityAudit(named: "ServerConfirmationScreen_Previews")
|
||||
}
|
||||
|
||||
func testServerSelection() async throws {
|
||||
try await performAccessibilityAudit(named: "ServerSelection_Previews")
|
||||
}
|
||||
@@ -663,6 +691,14 @@ extension AccessibilityTests {
|
||||
try await performAccessibilityAudit(named: "SessionVerification_Previews")
|
||||
}
|
||||
|
||||
func testSettingsScreenUserStatusPickerView() async throws {
|
||||
try await performAccessibilityAudit(named: "SettingsScreenUserStatusPickerView_Previews")
|
||||
}
|
||||
|
||||
func testSettingsScreenUserStatusRow() async throws {
|
||||
try await performAccessibilityAudit(named: "SettingsScreenUserStatusRow_Previews")
|
||||
}
|
||||
|
||||
func testSettingsScreen() async throws {
|
||||
try await performAccessibilityAudit(named: "SettingsScreen_Previews")
|
||||
}
|
||||
@@ -739,10 +775,6 @@ extension AccessibilityTests {
|
||||
try await performAccessibilityAudit(named: "SwipeRightAction_Previews")
|
||||
}
|
||||
|
||||
func testSwipeToReplyView() async throws {
|
||||
try await performAccessibilityAudit(named: "SwipeToReplyView_Previews")
|
||||
}
|
||||
|
||||
func testTextRoomTimelineView() async throws {
|
||||
try await performAccessibilityAudit(named: "TextRoomTimelineView_Previews")
|
||||
}
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
}
|
||||
],
|
||||
"defaultOptions" : {
|
||||
"addressSanitizer" : {
|
||||
"enabled" : true
|
||||
},
|
||||
"codeCoverage" : {
|
||||
"targets" : [
|
||||
{
|
||||
@@ -21,7 +18,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"defaultTestExecutionTimeAllowance" : 120,
|
||||
"defaultTestExecutionTimeAllowance" : 240,
|
||||
"environmentVariableEntries" : [
|
||||
|
||||
],
|
||||
@@ -36,8 +33,7 @@
|
||||
"identifier" : "C0C687DE1D270F9895FEE186",
|
||||
"name" : "AccessibilityTests"
|
||||
},
|
||||
"testTimeoutsEnabled" : true,
|
||||
"undefinedBehaviorSanitizerEnabled" : true
|
||||
"testTimeoutsEnabled" : true
|
||||
},
|
||||
"testTargets" : [
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ Only once all of the above is met should you open a PR with your proposed change
|
||||
* Don't submit large PRs, especially if not previously talked about. Anything above 200 lines is large (excluding generated code e.g. tests, translations, mocks)
|
||||
* Please don't open unfinished PRs and expect us to fill in the details
|
||||
* If you would like our opinion/direction on unfinished code please link your branch or idea in the ticket
|
||||
* Please limit the number of commits in a single PR. We are perfectly happy with splitting work across multiple sessions as long as they're logically independant and show promise of progress (ideally expressed through a ticket)
|
||||
* Please limit the number of commits in a single PR. We are perfectly happy with splitting work across multiple sessions as long as they're logically independent and show promise of progress (ideally expressed through a ticket)
|
||||
* Each and every commit should stand on its own, clearly explaining what it does and why
|
||||
* Once a PR goes into review please don't rewrite the history unless agreed so with the reviewer.
|
||||
* Tweaks and fixes following review can be directly committed (to be interactively rebased later) or as fixups
|
||||
@@ -95,6 +95,14 @@ git lfs install
|
||||
|
||||
If you make changes to the UI you may cause existing UI and Preview test snapshots to fail. The UITests run user flows and record snapshots while doing so using the settings defined under [checkEnvironments](https://github.com/element-hq/element-x-ios/blob/c29175d1f924e58b9646a200dbab0301fce3c258/UITests/Sources/Application.swift#L35-L37) while the PreviewTests use the settings defined in [PreviewTests.swift](https://github.com/element-hq/element-x-ios/blob/c29175d1f924e58b9646a200dbab0301fce3c258/PreviewTests/Sources/PreviewTests.swift#L18-L20). The snapshots are stored under `Sources/__Snapshots__` in their respective target's folder.
|
||||
|
||||
To run the preview tests:
|
||||
|
||||
1. Use an iPhone SE (3rd generation) simulator running iOS 26.5.
|
||||
2. On that simulator, go to Settings -> General -> Language & Region and select United States as the region, with English (US) as the only preferred language. This is a one time setup, as long as you keep using the same simulator.
|
||||
3. Select the `PreviewTests` scheme and run the tests.
|
||||
|
||||
Newly created screens will automatically have their screenshots added based off their `TestablePreview` Swift previews. If you are modifying an existing screen, you will first need to delete the old snapshots for that screen before running the tests. Alternatively, if you enable `RECORD_FAILURES` in the preview test plans's environment variables (`PreviewTests/SupportingFiles/Previewtests.xctestplan`), it will overwrite snapshot failures with the updated screenshot.
|
||||
|
||||
### Githooks
|
||||
|
||||
The project uses its own shared githooks stored in the .githooks folder, you will need to configure git to use such folder, this is already done if you have run the setup tool with `swift run tools setup-project` otherwise you would need to run:
|
||||
@@ -142,7 +150,7 @@ Our [changelog](CHANGES.md) is automatically generated by GitHub, based on the P
|
||||
For Swift coding style we use [SwiftLint](https://github.com/realm/SwiftLint) to check some conventions at compile time (rules are located in the `.swiftlint.yml` file).
|
||||
Otherwise please have a look to [Apple Swift conventions](https://swift.org/documentation/api-design-guidelines.html#conventions). We are also using some of the conventions of [raywenderlich.com Swift style guide](https://github.com/raywenderlich/swift-style-guide).
|
||||
|
||||
We enforce the coding style by running checks on the CI for every PR through [Danger](Dangerfile.swift), [SwiftLint](.swiftlint.yml), [SwiftFormat](.swiftformat) and [SonarCloud](https://sonarcloud.io/project/overview?id=element-x-ios)
|
||||
We enforce the coding style by running checks on the CI for every PR through [PR Checks](.github/workflows/pr-checks.yml), [SwiftLint](.swiftlint.yml), [SwiftFormat](.swiftformat) and [SonarCloud](https://sonarcloud.io/project/overview?id=element-x-ios)
|
||||
|
||||
We also gather coverage reports on every PR through [Codecov](https://app.codecov.io/gh/element-hq/element-x-ios) and will eventually start enforcing minimums.
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@ import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "BuildExtensions",
|
||||
platforms: [.macOS(.v15)],
|
||||
platforms: [
|
||||
.macOS(.v15),
|
||||
.iOS(.v18) // Only necessary for Previews
|
||||
],
|
||||
products: [
|
||||
.library(name: "Macros", targets: ["Macros"])
|
||||
],
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
/// Exposes a `Mutex`-backed hook as a read-only computed property, alongside a `register<Name>`
|
||||
/// peer that swaps in a replacement hook.
|
||||
///
|
||||
/// The `default` argument provides the hook used until (and unless) a replacement is registered.
|
||||
/// The enclosing type must have `Synchronization` in scope for the generated `Mutex` storage.
|
||||
@attached(accessor)
|
||||
@attached(peer, names: arbitrary)
|
||||
public macro AppHook<Value>(default: Value) =
|
||||
#externalMacro(module: "MacrosImplementation", type: "AppHookMacro")
|
||||
@@ -0,0 +1,98 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import SwiftSyntax
|
||||
import SwiftSyntaxMacros
|
||||
|
||||
/// Implements `@AppHook`. The generated property reads through a `Mutex`-backed store, and a
|
||||
/// generated `register<Name>` method swaps in a replacement hook.
|
||||
enum AppHookMacro {
|
||||
enum DiagnosticError: Error, CustomStringConvertible {
|
||||
case notAStoredProperty
|
||||
case missingTypeAnnotation
|
||||
case missingDefault
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .notAStoredProperty:
|
||||
"@AppHook can only be applied to a stored property."
|
||||
case .missingTypeAnnotation:
|
||||
"@AppHook requires an explicit type annotation."
|
||||
case .missingDefault:
|
||||
"@AppHook requires a default value."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct AppHook {
|
||||
let name: String
|
||||
let type: TypeSyntax
|
||||
let defaultValue: ExprSyntax
|
||||
|
||||
var storageName: TokenSyntax {
|
||||
"_\(raw: name)"
|
||||
}
|
||||
|
||||
var registerMethodName: TokenSyntax {
|
||||
"register\(raw: name.prefix(1).uppercased() + name.dropFirst())"
|
||||
}
|
||||
}
|
||||
|
||||
private static func appHook(for declaration: some DeclSyntaxProtocol, node: AttributeSyntax) throws -> AppHook {
|
||||
guard let variable = declaration.as(VariableDeclSyntax.self),
|
||||
let binding = variable.bindings.first,
|
||||
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
|
||||
throw DiagnosticError.notAStoredProperty
|
||||
}
|
||||
guard let type = binding.typeAnnotation?.type.trimmed else {
|
||||
throw DiagnosticError.missingTypeAnnotation
|
||||
}
|
||||
|
||||
var defaultValue: ExprSyntax?
|
||||
if let list = node.arguments?.as(LabeledExprListSyntax.self) {
|
||||
for argument in list where argument.label?.text == "default" {
|
||||
defaultValue = argument.expression
|
||||
}
|
||||
}
|
||||
guard let defaultValue else {
|
||||
throw DiagnosticError.missingDefault
|
||||
}
|
||||
|
||||
return AppHook(name: identifier.text, type: type, defaultValue: defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension AppHookMacro: AccessorMacro {
|
||||
static func expansion(of node: AttributeSyntax,
|
||||
providingAccessorsOf declaration: some DeclSyntaxProtocol,
|
||||
in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] {
|
||||
let appHook = try appHook(for: declaration, node: node)
|
||||
return [
|
||||
"""
|
||||
get {
|
||||
\(appHook.storageName).withLock { $0 }
|
||||
}
|
||||
"""
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
extension AppHookMacro: PeerMacro {
|
||||
static func expansion(of node: AttributeSyntax,
|
||||
providingPeersOf declaration: some DeclSyntaxProtocol,
|
||||
in context: some MacroExpansionContext) throws -> [DeclSyntax] {
|
||||
let appHook = try appHook(for: declaration, node: node)
|
||||
return [
|
||||
"private let \(appHook.storageName) = Mutex<\(appHook.type)>(\(appHook.defaultValue))",
|
||||
"""
|
||||
func \(appHook.registerMethodName)(_ hook: \(appHook.type)) {
|
||||
\(appHook.storageName).withLock { $0 = hook }
|
||||
}
|
||||
"""
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -10,5 +10,5 @@ import SwiftSyntaxMacros
|
||||
|
||||
@main
|
||||
struct BuildExtensionsPlugin: CompilerPlugin {
|
||||
let providingMacros: [Macro.Type] = [UserPreferenceMacro.self]
|
||||
let providingMacros: [Macro.Type] = [UserPreferenceMacro.self, AppHookMacro.self]
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import SwiftSyntaxMacros
|
||||
///
|
||||
/// A `volatile` preference instead reads and writes through a generated in-memory store, so its
|
||||
/// value is never persisted and resets on each launch.
|
||||
public enum UserPreferenceMacro {
|
||||
enum UserPreferenceMacro {
|
||||
enum DiagnosticError: Error, CustomStringConvertible {
|
||||
case notAStoredProperty
|
||||
case missingTypeAnnotation
|
||||
@@ -85,9 +85,9 @@ public enum UserPreferenceMacro {
|
||||
}
|
||||
|
||||
extension UserPreferenceMacro: AccessorMacro {
|
||||
public static func expansion(of node: AttributeSyntax,
|
||||
providingAccessorsOf declaration: some DeclSyntaxProtocol,
|
||||
in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] {
|
||||
static func expansion(of node: AttributeSyntax,
|
||||
providingAccessorsOf declaration: some DeclSyntaxProtocol,
|
||||
in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] {
|
||||
let preference = try preference(for: declaration, node: node)
|
||||
|
||||
if preference.isVolatile {
|
||||
@@ -137,9 +137,9 @@ extension UserPreferenceMacro: AccessorMacro {
|
||||
}
|
||||
|
||||
extension UserPreferenceMacro: PeerMacro {
|
||||
public static func expansion(of node: AttributeSyntax,
|
||||
providingPeersOf declaration: some DeclSyntaxProtocol,
|
||||
in context: some MacroExpansionContext) throws -> [DeclSyntax] {
|
||||
static func expansion(of node: AttributeSyntax,
|
||||
providingPeersOf declaration: some DeclSyntaxProtocol,
|
||||
in context: some MacroExpansionContext) throws -> [DeclSyntax] {
|
||||
let preference = try preference(for: declaration, node: node)
|
||||
let type = preference.type
|
||||
let name = preference.name
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0b1465b1e379f4356e0cbde2ecc1a9cd2a58c7a69be7ca9f7b3b2df2e911e60c
|
||||
size 2004302
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// Implemented by MapLibreShim's entry point, which the app instantiates after
|
||||
/// `dlopen`-ing the framework.
|
||||
public protocol MapLibreShimProtocol {
|
||||
/// Routes MapLibre's logs into the app's logger. Call once after loading the shim.
|
||||
func configureLogging(_ handler: @escaping @Sendable (MapLogSeverity, String) -> Void)
|
||||
|
||||
/// Builds the interactive map view. The app-side `MapLibreMapView` forwards every
|
||||
/// body evaluation here so SwiftUI updates flow through to the underlying map.
|
||||
func makeMapView(configuration: MapViewConfiguration) -> AnyView
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
// Copyright 2023-2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
Behavior mode of the current user's location, can be hidden, only shown and shown following the user
|
||||
*/
|
||||
public enum ShowUserLocationMode: Equatable {
|
||||
/// this mode will show the user pin in map
|
||||
case show
|
||||
/// this mode will show the user pin in map and track him, panning the map automatically
|
||||
case showAndFollow
|
||||
/// this mode will not show the user pin in map
|
||||
case hide
|
||||
/// this mode will not show the user pin in map and will follow the marker with the given id,
|
||||
/// panning the map automatically.
|
||||
case hideAndFollowMarker(id: String)
|
||||
|
||||
/// The id of the marker the map should follow when in the `hideAndFollowMarker` mode, nil otherwise.
|
||||
public var followedMarkerID: String? {
|
||||
guard case .hideAndFollowMarker(let id) = self else { return nil }
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
public enum MapLibreError: Error, Hashable {
|
||||
case failedLoadingMap
|
||||
case failedLocatingUser
|
||||
}
|
||||
|
||||
/// The severity of a map log message forwarded to the app.
|
||||
public enum MapLogSeverity {
|
||||
case error
|
||||
case warning
|
||||
case info
|
||||
case debug
|
||||
case verbose
|
||||
}
|
||||
|
||||
/// A marker shown on the map, identified so that it can be moved in place when its coordinate changes.
|
||||
public struct MapAnnotation {
|
||||
public let id: String
|
||||
public let coordinate: CLLocationCoordinate2D
|
||||
|
||||
public init(id: String, coordinate: CLLocationCoordinate2D) {
|
||||
self.id = id
|
||||
self.coordinate = coordinate
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import SwiftUI
|
||||
|
||||
/// Everything the map implementation needs in order to render, expressed without app
|
||||
/// types so that the implementation framework can be loaded on demand.
|
||||
public struct MapViewConfiguration {
|
||||
/// the final zoom level used when the first user location emit
|
||||
public let zoomLevel: Double
|
||||
/// The initial zoom level used when the map it firstly loaded and the user location is not yet available, in case of annotations this property is not being used
|
||||
public let initialZoomLevel: Double
|
||||
|
||||
/// The initial map center
|
||||
public let mapCenter: CLLocationCoordinate2D
|
||||
|
||||
/// Map annotations
|
||||
public let annotations: [MapAnnotation]
|
||||
|
||||
/// The style to load the map with, for either the dark or the light appearance.
|
||||
public let styleURL: (_ isDark: Bool) -> URL?
|
||||
|
||||
public let tintColor: UIColor
|
||||
|
||||
/// The content of the marker representing the annotation with the given id.
|
||||
public let markerView: (_ annotationID: String) -> AnyView?
|
||||
|
||||
/// Behavior mode of the current user's location, can be hidden, only shown and shown following the user
|
||||
public let showsUserLocationMode: Binding<ShowUserLocationMode>
|
||||
/// Bind view errors if any
|
||||
public let error: Binding<MapLibreError?>
|
||||
/// Coordinate of the center of the map
|
||||
public let mapCenterCoordinate: Binding<CLLocationCoordinate2D?>
|
||||
public let hasLoadedUserLocation: Binding<Bool>
|
||||
public let isLocationAuthorized: Binding<Bool?>
|
||||
/// The radius of uncertainty for the location, measured in meters.
|
||||
public let geolocationUncertainty: Binding<CLLocationAccuracy?>
|
||||
|
||||
/// Called when the user pan on the map
|
||||
public let userDidPan: (() -> Void)?
|
||||
|
||||
public init(zoomLevel: Double,
|
||||
initialZoomLevel: Double,
|
||||
mapCenter: CLLocationCoordinate2D,
|
||||
annotations: [MapAnnotation] = [],
|
||||
styleURL: @escaping (_ isDark: Bool) -> URL?,
|
||||
tintColor: UIColor,
|
||||
markerView: @escaping (_ annotationID: String) -> AnyView?,
|
||||
showsUserLocationMode: Binding<ShowUserLocationMode>,
|
||||
error: Binding<MapLibreError?>,
|
||||
mapCenterCoordinate: Binding<CLLocationCoordinate2D?>,
|
||||
hasLoadedUserLocation: Binding<Bool>,
|
||||
isLocationAuthorized: Binding<Bool?>,
|
||||
geolocationUncertainty: Binding<CLLocationAccuracy?>,
|
||||
userDidPan: (() -> Void)? = nil) {
|
||||
self.zoomLevel = zoomLevel
|
||||
self.initialZoomLevel = initialZoomLevel
|
||||
self.mapCenter = mapCenter
|
||||
self.annotations = annotations
|
||||
self.styleURL = styleURL
|
||||
self.tintColor = tintColor
|
||||
self.markerView = markerView
|
||||
self.showsUserLocationMode = showsUserLocationMode
|
||||
self.error = error
|
||||
self.mapCenterCoordinate = mapCenterCoordinate
|
||||
self.hasLoadedUserLocation = hasLoadedUserLocation
|
||||
self.isLocationAuthorized = isLocationAuthorized
|
||||
self.geolocationUncertainty = geolocationUncertainty
|
||||
self.userDidPan = userDidPan
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# The types shared between the app and the MapLibreShim framework. The app links this
|
||||
# (tiny, initialiser-free) framework instead of MapLibre, see MapLibreShim's target.yml.
|
||||
name: MapLibreInterface
|
||||
|
||||
targets:
|
||||
MapLibreInterface:
|
||||
type: framework
|
||||
platform: iOS
|
||||
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: $(BASE_BUNDLE_IDENTIFIER).maplibre-interface
|
||||
GENERATE_INFOPLIST_FILE: YES
|
||||
SWIFT_VERSION: 6.2
|
||||
SWIFT_APPROACHABLE_CONCURRENCY: YES
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION: MainActor
|
||||
|
||||
sources:
|
||||
- path: ../Sources
|
||||
@@ -8,7 +8,7 @@
|
||||
import CoreLocation
|
||||
import QuartzCore
|
||||
|
||||
/// Smoothly animates a `LocationAnnotation`'s coordinate from its current
|
||||
/// Smoothly animates a `MapLibreAnnotation`'s coordinate from its current
|
||||
/// position to a new one using a `CADisplayLink` for frame-accurate updates.
|
||||
///
|
||||
/// ## How it works
|
||||
@@ -26,7 +26,7 @@ import QuartzCore
|
||||
/// cancels the previous one and begins from the annotation's current position.
|
||||
final class CoordinateAnimator {
|
||||
private var displayLink: CADisplayLink?
|
||||
private weak var annotation: LocationAnnotation?
|
||||
private weak var annotation: MapLibreAnnotation?
|
||||
private let startCoordinate: CLLocationCoordinate2D
|
||||
private let endCoordinate: CLLocationCoordinate2D
|
||||
private let duration: CFTimeInterval
|
||||
@@ -36,7 +36,7 @@ final class CoordinateAnimator {
|
||||
/// while the display link is running. Keyed by annotation ID.
|
||||
private static var activeAnimators: [String: CoordinateAnimator] = [:]
|
||||
|
||||
private init(annotation: LocationAnnotation,
|
||||
private init(annotation: MapLibreAnnotation,
|
||||
to end: CLLocationCoordinate2D,
|
||||
duration: CFTimeInterval) {
|
||||
self.annotation = annotation
|
||||
@@ -48,7 +48,7 @@ final class CoordinateAnimator {
|
||||
/// Starts animating the annotation's coordinate to `end` over `duration` seconds.
|
||||
/// If the annotation is already being animated, the in-flight animation is
|
||||
/// cancelled and a new one starts from the current position.
|
||||
static func animate(annotation: LocationAnnotation,
|
||||
static func animate(annotation: MapLibreAnnotation,
|
||||
to end: CLLocationCoordinate2D,
|
||||
duration: CFTimeInterval) {
|
||||
guard annotation.coordinate.latitude != end.latitude
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
// Copyright 2023-2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import MapLibre
|
||||
import MapLibreInterface
|
||||
import SwiftUI
|
||||
|
||||
final class MapLibreAnnotation: NSObject, MLNAnnotation, Identifiable {
|
||||
let id: String
|
||||
// @objc dynamic is required to make animations work
|
||||
@objc dynamic var coordinate: CLLocationCoordinate2D
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(_ annotation: MapAnnotation) {
|
||||
id = annotation.id
|
||||
coordinate = annotation.coordinate
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
final class MapLibreAnnotationView: MLNUserLocationAnnotationView {
|
||||
private var hostingController: UIHostingController<AnyView>?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
override init(annotation: MLNAnnotation?, reuseIdentifier: String?) {
|
||||
super.init(annotation: annotation, reuseIdentifier:
|
||||
reuseIdentifier)
|
||||
}
|
||||
|
||||
convenience init(annotation: MapLibreAnnotation, content: AnyView?) {
|
||||
self.init(annotation: annotation, reuseIdentifier: "\(Self.self)")
|
||||
let hostingController = UIHostingController(rootView: content ?? AnyView(EmptyView()))
|
||||
self.hostingController = hostingController
|
||||
let view: UIView = hostingController.view
|
||||
view.backgroundColor = .clear
|
||||
view.anchorPoint = .init(x: 0.5, y: 1.0)
|
||||
addSubview(view)
|
||||
view.bounds.size = view.intrinsicContentSize
|
||||
}
|
||||
|
||||
func updateContent(_ content: AnyView?) {
|
||||
hostingController?.rootView = content ?? AnyView(EmptyView())
|
||||
if let hostedView = hostingController?.view {
|
||||
hostedView.bounds.size = hostedView.intrinsicContentSize
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
// Copyright 2023-2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import MapLibre
|
||||
import MapLibreInterface
|
||||
import SwiftUI
|
||||
|
||||
struct MapLibreMapView: UIViewRepresentable {
|
||||
// MARK: - Properties
|
||||
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
let configuration: MapViewConfiguration
|
||||
|
||||
// MARK: - UIViewRepresentable
|
||||
|
||||
func makeUIView(context: Context) -> MLNMapView {
|
||||
let mapView = makeMapView()
|
||||
mapView.delegate = context.coordinator
|
||||
setupMap(mapView: mapView)
|
||||
return mapView
|
||||
}
|
||||
|
||||
func updateUIView(_ mapView: MLNMapView, context: Context) {
|
||||
// Don't set the same value twice. Otherwise, if there is an error loading the map, a loop
|
||||
// is caused as the `error` binding being set, which triggers this update, which sets a
|
||||
// new URL, which causes another error, and so it goes on round and round in a circle.
|
||||
let dynamicMapURL = configuration.styleURL(colorScheme == .dark)
|
||||
if mapView.styleURL != dynamicMapURL {
|
||||
mapView.styleURL = dynamicMapURL
|
||||
}
|
||||
|
||||
// If the center coordinate was updated externally (not by the map itself), move the map.
|
||||
// Not applied while following a marker, where the camera is driven by the marker's position.
|
||||
if configuration.showsUserLocationMode.wrappedValue.followedMarkerID == nil,
|
||||
let newCenter = configuration.mapCenterCoordinate.wrappedValue,
|
||||
newCenter.latitude != context.coordinator.lastReportedCenter?.latitude
|
||||
|| newCenter.longitude != context.coordinator.lastReportedCenter?.longitude {
|
||||
context.coordinator.lastReportedCenter = newCenter
|
||||
mapView.setCenter(newCenter, animated: true)
|
||||
}
|
||||
|
||||
// Update existing annotation views with fresh SwiftUI content.
|
||||
// This handles the case where the annotation's view data changes after
|
||||
// the annotation was initially placed (e.g. user avatar loads asynchronously).
|
||||
updateAnnotations(in: mapView)
|
||||
|
||||
showUserLocation(in: mapView)
|
||||
|
||||
context.coordinator.updateMarkerFollowing(in: mapView, markerID: configuration.showsUserLocationMode.wrappedValue.followedMarkerID)
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupMap(mapView: MLNMapView) {
|
||||
mapView.addAnnotations(configuration.annotations.map(MapLibreAnnotation.init))
|
||||
mapView.zoomLevel = configuration.annotations.isEmpty ? configuration.initialZoomLevel : configuration.zoomLevel
|
||||
mapView.centerCoordinate = configuration.mapCenter
|
||||
}
|
||||
|
||||
private func updateAnnotations(in mapView: MLNMapView) {
|
||||
let existingByID = Dictionary(uniqueKeysWithValues:
|
||||
(mapView.annotations ?? []).compactMap { $0 as? MapLibreAnnotation }.map { ($0.id, $0) })
|
||||
let updatedByID = Dictionary(uniqueKeysWithValues: configuration.annotations.map { ($0.id, $0) })
|
||||
|
||||
let existingIDs = Set(existingByID.keys)
|
||||
let updatedIDs = Set(updatedByID.keys)
|
||||
|
||||
// Remove annotations that are no longer present
|
||||
let removedIDs = existingIDs.subtracting(updatedIDs)
|
||||
if !removedIDs.isEmpty {
|
||||
let toRemove = removedIDs.compactMap { existingByID[$0] }
|
||||
mapView.removeAnnotations(toRemove)
|
||||
}
|
||||
|
||||
// Add new annotations
|
||||
let addedIDs = updatedIDs.subtracting(existingIDs)
|
||||
if !addedIDs.isEmpty {
|
||||
let toAdd = addedIDs.compactMap { updatedByID[$0] }.map(MapLibreAnnotation.init)
|
||||
mapView.addAnnotations(toAdd)
|
||||
}
|
||||
|
||||
// Update existing annotations that are still present
|
||||
let keptIDs = existingIDs.intersection(updatedIDs)
|
||||
for id in keptIDs {
|
||||
guard let existingAnnotation = existingByID[id],
|
||||
let updatedAnnotation = updatedByID[id] else {
|
||||
continue
|
||||
}
|
||||
CoordinateAnimator.animate(annotation: existingAnnotation,
|
||||
to: updatedAnnotation.coordinate,
|
||||
duration: 1.0)
|
||||
if let annotationView = mapView.view(for: existingAnnotation) as? MapLibreAnnotationView {
|
||||
annotationView.updateContent(configuration.markerView(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func makeMapView() -> MLNMapView {
|
||||
let mapView = MLNMapView(frame: .zero, styleURL: configuration.styleURL(colorScheme == .dark))
|
||||
mapView.logoViewPosition = .topLeft
|
||||
mapView.attributionButtonPosition = .topLeft
|
||||
mapView.attributionButtonMargins = .init(x: mapView.logoView.frame.maxX + 8, y: mapView.logoView.center.y / 2)
|
||||
mapView.tintColor = configuration.tintColor
|
||||
mapView.allowsRotating = false
|
||||
mapView.allowsTilting = false
|
||||
return mapView
|
||||
}
|
||||
|
||||
private func showUserLocation(in mapView: MLNMapView) {
|
||||
switch (configuration.showsUserLocationMode.wrappedValue, configuration.annotations) {
|
||||
case (.showAndFollow, _):
|
||||
mapView.userTrackingMode = .follow
|
||||
case (.show, let annotations) where !annotations.isEmpty:
|
||||
// In the show mode, if there are annotations, we check the authorizationStatus,
|
||||
// if it's not determined, we wont prompt the user with a request for permissions,
|
||||
// because they should be able to see the annotations without sharing their location information.
|
||||
guard mapView.locationManager.authorizationStatus != .notDetermined else { return }
|
||||
fallthrough
|
||||
case (.show, _):
|
||||
mapView.showsUserLocation = true
|
||||
mapView.setUserTrackingMode(.none, animated: false, completionHandler: nil)
|
||||
case (.hide, _), (.hideAndFollowMarker, _):
|
||||
// In hideAndFollowMarker mode the camera following is handled by the coordinator.
|
||||
mapView.showsUserLocation = false
|
||||
mapView.setUserTrackingMode(.none, animated: false, completionHandler: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Coordinator
|
||||
|
||||
extension MapLibreMapView {
|
||||
class Coordinator: NSObject, MLNMapViewDelegate {
|
||||
// MARK: - Properties
|
||||
|
||||
var mapLibreView: MapLibreMapView
|
||||
|
||||
private var previousUserLocation: MLNUserLocation?
|
||||
/// Tracks the last center coordinate reported by the map (or set programmatically),
|
||||
/// so that `updateUIView` can tell apart external binding changes from internal ones.
|
||||
var lastReportedCenter: CLLocationCoordinate2D?
|
||||
|
||||
/// The annotation the camera is currently locked on while in the `hideAndFollowMarker` mode.
|
||||
private weak var followedAnnotation: MapLibreAnnotation?
|
||||
/// Observes the followed annotation's coordinate, updated every frame by the `CoordinateAnimator`.
|
||||
private var followedAnnotationObservation: NSKeyValueObservation?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(_ mapLibreView: MapLibreMapView) {
|
||||
self.mapLibreView = mapLibreView
|
||||
}
|
||||
|
||||
// MARK: - Marker following
|
||||
|
||||
/// Keeps the camera locked on the marker with the given id, or stops following when nil.
|
||||
func updateMarkerFollowing(in mapView: MLNMapView, markerID: String?) {
|
||||
guard let markerID else {
|
||||
stopFollowingMarker()
|
||||
return
|
||||
}
|
||||
|
||||
guard let annotation = (mapView.annotations ?? [])
|
||||
.compactMap({ $0 as? MapLibreAnnotation })
|
||||
.first(where: { $0.id == markerID }) else {
|
||||
// The marker isn't on the map (yet), resolve it again on the next update.
|
||||
stopFollowingMarker()
|
||||
return
|
||||
}
|
||||
|
||||
guard annotation !== followedAnnotation else { return }
|
||||
startFollowingMarker(annotation, in: mapView)
|
||||
}
|
||||
|
||||
private func startFollowingMarker(_ annotation: MapLibreAnnotation, in mapView: MLNMapView) {
|
||||
followedAnnotation = annotation
|
||||
followedAnnotationObservation = nil
|
||||
|
||||
// Animate to the marker first, attaching the per-frame tracking only on completion
|
||||
// so that it doesn't cancel the transition.
|
||||
mapView.setCenter(annotation.coordinate,
|
||||
zoomLevel: mapView.zoomLevel,
|
||||
direction: -1, // negative value keeps the current direction
|
||||
animated: true) { [weak self, weak annotation, weak mapView] in
|
||||
guard let self, let annotation, let mapView, annotation === followedAnnotation else { return }
|
||||
|
||||
// Mirror every animated coordinate update to keep the camera as smooth as the marker.
|
||||
followedAnnotationObservation = annotation.observe(\.coordinate) { [weak mapView] annotation, _ in
|
||||
// The coordinate is only ever updated by SwiftUI so KVO fires on the main actor.
|
||||
MainActor.assumeIsolated {
|
||||
mapView?.setCenter(annotation.coordinate, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopFollowingMarker() {
|
||||
followedAnnotation = nil
|
||||
followedAnnotationObservation = nil
|
||||
}
|
||||
|
||||
// MARK: - MLNMapViewDelegate
|
||||
|
||||
func mapView(_ mapView: MLNMapView, viewFor annotation: MLNAnnotation) -> MLNAnnotationView? {
|
||||
guard let annotation = annotation as? MapLibreAnnotation else {
|
||||
return nil
|
||||
}
|
||||
return MapLibreAnnotationView(annotation: annotation, content: mapLibreView.configuration.markerView(annotation.id))
|
||||
}
|
||||
|
||||
func mapViewDidFailLoadingMap(_ mapView: MLNMapView, withError error: Error) {
|
||||
if mapLibreView.configuration.error.wrappedValue != .failedLoadingMap {
|
||||
mapLibreView.configuration.error.wrappedValue = .failedLoadingMap
|
||||
}
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MLNMapView, didUpdate userLocation: MLNUserLocation?) {
|
||||
guard let userLocation else { return }
|
||||
mapLibreView.configuration.hasLoadedUserLocation.wrappedValue = true
|
||||
|
||||
if previousUserLocation == nil, mapLibreView.configuration.annotations.isEmpty {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
mapView.setCenter(userLocation.coordinate, zoomLevel: self.mapLibreView.configuration.zoomLevel, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
previousUserLocation = userLocation
|
||||
updateGeolocationUncertainty(location: userLocation)
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MLNMapView, didChangeLocationManagerAuthorization manager: MLNLocationManager) {
|
||||
switch manager.authorizationStatus {
|
||||
case .denied, .restricted:
|
||||
mapLibreView.configuration.isLocationAuthorized.wrappedValue = false
|
||||
case .authorizedAlways, .authorizedWhenInUse:
|
||||
mapLibreView.configuration.isLocationAuthorized.wrappedValue = true
|
||||
case .notDetermined:
|
||||
mapLibreView.configuration.isLocationAuthorized.wrappedValue = nil
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MLNMapView, regionDidChangeAnimated animated: Bool) {
|
||||
// While following a marker, don't flood the center binding with per-frame camera updates.
|
||||
// Leaving `lastReportedCenter` untouched avoids an external re-centering when following stops.
|
||||
guard mapLibreView.configuration.showsUserLocationMode.wrappedValue.followedMarkerID == nil else { return }
|
||||
|
||||
// Avoid `Publishing changes from within view update` warnings
|
||||
DispatchQueue.main.async { [mapLibreView, weak self] in
|
||||
let center = mapView.centerCoordinate
|
||||
self?.lastReportedCenter = center
|
||||
mapLibreView.configuration.mapCenterCoordinate.wrappedValue = center
|
||||
}
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MLNMapView, shouldChangeFrom oldCamera: MLNMapCamera, to newCamera: MLNMapCamera, reason: MLNCameraChangeReason) -> Bool {
|
||||
// Send userDidPan only for gestures that actually change the map center. The reason
|
||||
// is an option set and gestures can come combined with other reasons, so check for
|
||||
// containment instead of an exact match.
|
||||
let centerChangingGestures: MLNCameraChangeReason = [.gesturePan, .gesturePinch, .gestureRotate]
|
||||
if !reason.isDisjoint(with: centerChangingGestures) {
|
||||
// Stop following immediately, the camera would fight the gesture otherwise.
|
||||
stopFollowingMarker()
|
||||
mapLibreView.configuration.userDidPan?()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: Callout
|
||||
|
||||
func mapView(_ mapView: MLNMapView, annotationCanShowCallout annotation: MLNAnnotation) -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private func updateGeolocationUncertainty(location: MLNUserLocation) {
|
||||
guard let clLocation = location.location, clLocation.horizontalAccuracy >= 0 else {
|
||||
mapLibreView.configuration.geolocationUncertainty.wrappedValue = nil
|
||||
return
|
||||
}
|
||||
|
||||
mapLibreView.configuration.geolocationUncertainty.wrappedValue = clLocation.horizontalAccuracy
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import MapLibre
|
||||
import MapLibreInterface
|
||||
import SwiftUI
|
||||
|
||||
/// The shim's entry point: the app looks this class up by name through the ObjC runtime
|
||||
/// after `dlopen`-ing the framework, keeping MapLibre off the app's launch path.
|
||||
@objc(MapLibreShim)
|
||||
public final class MapLibreShim: NSObject, MapLibreShimProtocol {
|
||||
public func configureLogging(_ handler: @escaping @Sendable (MapLogSeverity, String) -> Void) {
|
||||
MLNLoggingConfiguration.shared.loggingLevel = .debug
|
||||
MLNLoggingConfiguration.shared.handler = { loggingLevel, _, _, message in
|
||||
switch loggingLevel {
|
||||
case .error:
|
||||
handler(.error, message)
|
||||
case .warning:
|
||||
handler(.warning, message)
|
||||
case .info:
|
||||
handler(.info, message)
|
||||
case .debug:
|
||||
handler(.debug, message)
|
||||
case .verbose:
|
||||
handler(.verbose, message)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func makeMapView(configuration: MapViewConfiguration) -> AnyView {
|
||||
AnyView(MapLibreMapView(configuration: configuration))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# The interactive map, split out of the app so MapLibre's static initialisers stay off the
|
||||
# launch path: the app links only the tiny MapLibreInterface framework, while MapLibreShim (the
|
||||
# only image linking MapLibre) is embedded unlinked and dlopen'd on first map use.
|
||||
name: MapLibreShim
|
||||
|
||||
targets:
|
||||
MapLibreShim:
|
||||
type: framework
|
||||
platform: iOS
|
||||
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: $(BASE_BUNDLE_IDENTIFIER).maplibre-shim
|
||||
GENERATE_INFOPLIST_FILE: YES
|
||||
SWIFT_VERSION: 6.2
|
||||
SWIFT_APPROACHABLE_CONCURRENCY: YES
|
||||
SWIFT_DEFAULT_ACTOR_ISOLATION: MainActor
|
||||
|
||||
dependencies:
|
||||
- target: MapLibreInterface
|
||||
embed: false
|
||||
- package: MapLibre
|
||||
embed: false
|
||||
|
||||
sources:
|
||||
- path: ../Sources
|
||||
|
Before Width: | Height: | Size: 387 KiB After Width: | Height: | Size: 387 KiB |
|
Before Width: | Height: | Size: 572 B After Width: | Height: | Size: 572 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 818 B After Width: | Height: | Size: 818 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
@@ -1,52 +0,0 @@
|
||||
import Danger
|
||||
import Foundation
|
||||
|
||||
SwiftLint.lint(.modifiedAndCreatedFiles(directory: nil),
|
||||
inline: true,
|
||||
configFile: nil,
|
||||
strict: false,
|
||||
quiet: true,
|
||||
swiftlintPath: nil,
|
||||
markdownAction: { _ in })
|
||||
|
||||
let danger = Danger()
|
||||
|
||||
/// All of the new and modified files together.
|
||||
let editedFiles = danger.git.modifiedFiles + danger.git.createdFiles
|
||||
|
||||
// Warn when there is a big PR
|
||||
if (danger.github.pullRequest.additions ?? 0) > 1000 {
|
||||
warn("This pull request seems relatively large. Please consider splitting it into multiple smaller ones.")
|
||||
}
|
||||
|
||||
// Check that the PR has a description
|
||||
if danger.github.pullRequest.body?.isEmpty ?? true {
|
||||
warn("Please provide a description for this PR.")
|
||||
}
|
||||
|
||||
/// Check for screenshots on view changes
|
||||
let hasChangedViews = editedFiles.contains { $0.lowercased().contains("/view") }
|
||||
if hasChangedViews {
|
||||
if (danger.github.pullRequest.body?.contains("user-attachments") ?? false) == false {
|
||||
warn("You seem to have made changes to views. Please consider adding screenshots.")
|
||||
}
|
||||
}
|
||||
|
||||
/// Check for pngs on resources
|
||||
let hasPngs = editedFiles.contains { $0.lowercased().contains(".xcassets") && $0.lowercased().hasSuffix(".png") }
|
||||
if hasPngs {
|
||||
warn("You seem to have made changes to some resource images. Please consider using an SVG or PDF.")
|
||||
}
|
||||
|
||||
// Check for nice PR titles
|
||||
let prTitle = danger.github.pullRequest.title
|
||||
let fixesRegex = try! Regex("(Fixes|Fix) #\\d+")
|
||||
let semanticRegex = try! Regex("\\w+\\(\\w+\\):")
|
||||
if prTitle.hasSuffix("…") || prTitle.starts(with: fixesRegex) || prTitle.starts(with: semanticRegex) {
|
||||
fail("Please provide a complete title that can be used as a changelog entry.")
|
||||
}
|
||||
|
||||
// Check for changelog tags
|
||||
if danger.github.issue.labels.filter({ $0.name.hasPrefix("pr-") }).count != 1 {
|
||||
fail("Please add a `pr-` label to categorise the changelog entry.")
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cc39671ee91a08779e4149589a4635fa6b91519b07bfb2dee621ccab031a75c5
|
||||
size 5825201
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"originHash" : "b2bcfd4f704c144b36ac18bb17859627163be8018ed52c4ec3cbf11d3e3f4469",
|
||||
"originHash" : "f1791683f9f09408a7746ce0e163941b3ce7e83e84f1a73b2fecc2b5d2f2f4b6",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "compound-design-tokens",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/element-hq/compound-design-tokens",
|
||||
"state" : {
|
||||
"revision" : "bc390b74b53447f04bc0a7a07adaecbb90e02e25",
|
||||
"version" : "10.2.3"
|
||||
"revision" : "86c15ef71798581d1e0cc39a7f99e598ad572419",
|
||||
"version" : "11.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -60,8 +60,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/element-hq/element-call-swift",
|
||||
"state" : {
|
||||
"revision" : "d2ecef5a54e5718ed6344dde6f72a4da42f1fcd9",
|
||||
"version" : "0.20.2"
|
||||
"revision" : "13eac3567461a9825018e10e9f4475f68215dae7",
|
||||
"version" : "0.24.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -95,8 +95,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/onevcat/Kingfisher",
|
||||
"state" : {
|
||||
"revision" : "ac632bd26a1c00f139ff62fd01806f21cf67325e",
|
||||
"version" : "8.10.0"
|
||||
"revision" : "410984bf301f4fa224fe56277b3f8672cc465c79",
|
||||
"version" : "8.11.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -131,8 +131,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/guoyingtao/Mantis",
|
||||
"state" : {
|
||||
"revision" : "b3c237aff54b8307440157a2f5956bdea2e76791",
|
||||
"version" : "2.31.2"
|
||||
"revision" : "0960be1feed2e62f62a5fddbb8d72b26008db881",
|
||||
"version" : "3.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -140,8 +140,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution",
|
||||
"state" : {
|
||||
"revision" : "84a79bc375a301169390ac110c868f06c857b83f",
|
||||
"version" : "6.27.0"
|
||||
"revision" : "5ee345ca5d65238a6fce29bba87816204be7df20",
|
||||
"version" : "6.28.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -167,8 +167,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/element-hq/matrix-rust-components-swift",
|
||||
"state" : {
|
||||
"revision" : "b286aeac2774f516839abcb91bbac1450188a591",
|
||||
"version" : "26.7.8"
|
||||
"revision" : "56ec8d0458a688018bbd9f1f2fd70fae7919f415",
|
||||
"version" : "26.9.9"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -194,8 +194,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/PostHog/posthog-ios",
|
||||
"state" : {
|
||||
"revision" : "6e70aa24bbd3778182fe039f7f5800a17674a6d6",
|
||||
"version" : "3.61.1"
|
||||
"revision" : "c0218386c49115cce6b05a50b41f4f60bc995acb",
|
||||
"version" : "3.69.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -203,8 +203,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/getsentry/sentry-cocoa",
|
||||
"state" : {
|
||||
"revision" : "43abfbf9a26089ad34604c718ad0935440caf1d4",
|
||||
"version" : "9.18.0"
|
||||
"revision" : "cd213f98af160e6497eeff0707bc3f6a205b159c",
|
||||
"version" : "9.26.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -230,17 +230,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-async-algorithms",
|
||||
"state" : {
|
||||
"revision" : "d0b4a06d0f173a2f3be27d3ea21b3c3aa18db440",
|
||||
"version" : "1.1.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-clocks",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-clocks",
|
||||
"state" : {
|
||||
"revision" : "72d749bf341b78851203066ab421869b783ec42a",
|
||||
"version" : "1.1.0"
|
||||
"revision" : "3da39bbc4e687d4192af7c9cf4eab805745a0b9c",
|
||||
"version" : "1.1.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -252,15 +243,6 @@
|
||||
"version" : "1.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-concurrency-extras",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
|
||||
"state" : {
|
||||
"revision" : "5a3825302b1a0d744183200915a47b508c828e6f",
|
||||
"version" : "1.3.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-custom-dump",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -293,8 +275,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
|
||||
"state" : {
|
||||
"revision" : "ad5e3190cc63dc288f28546f9c6827efc1e9d495",
|
||||
"version" : "1.19.2"
|
||||
"revision" : "59a99c458de4d2dee580529b61b4f78dca7b7fa6",
|
||||
"version" : "1.19.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -311,8 +293,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/scinfu/SwiftSoup.git",
|
||||
"state" : {
|
||||
"revision" : "49dcadd93161f4a44b4994d3a3e8de9f085aface",
|
||||
"version" : "2.13.5"
|
||||
"revision" : "8d6ad267714cac3ae747cefdd21f7a6665006e1f",
|
||||
"version" : "2.13.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -329,8 +311,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tevelee/SwiftUI-Flow.git",
|
||||
"state" : {
|
||||
"revision" : "798364823cdfc6afb4aff43eff5a3ddbca89b54f",
|
||||
"version" : "3.4.0"
|
||||
"revision" : "19e86b750b3c67260cbb28c459004c0dfdb11899",
|
||||
"version" : "3.5.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -338,8 +320,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/siteline/SwiftUI-Introspect",
|
||||
"state" : {
|
||||
"revision" : "26986a57e31c813f98262ddb62d4b07d40008535",
|
||||
"version" : "26.0.1"
|
||||
"revision" : "dd0888ef99d2809863b04e46cad52d1dfd8fc840",
|
||||
"version" : "26.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -87,6 +87,11 @@
|
||||
value = "localhost:9090"
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "HTTP_PROXY"
|
||||
value = "localhost:9090"
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "UI_TESTS_SCREEN"
|
||||
value = ""
|
||||
|
||||
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 265 KiB |
@@ -1,15 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "app-logo.pdf",
|
||||
"idiom" : "universal"
|
||||
"filename" : "app-logo.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "app-logo@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "app-logo@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 59 KiB |
@@ -0,0 +1,6 @@
|
||||
"NSCameraUsageDescription" = "يحتاج Element X إلى الوصول إلى الكاميرا لالتقاط الصور أو مقاطع الفيديو وإرسالها كرسائل.";
|
||||
"NSFaceIDUsageDescription" = "يُستخدم Face ID لحماية الوصول إلى حسابك.";
|
||||
"NSLocationAlwaysAndWhenInUseUsageDescription" = "لمشاركة موقعك المباشر، يحتاج Element X إلى الوصول إلى الموقع عندما يكون التطبيق في الخلفية.";
|
||||
"NSLocationWhenInUseUsageDescription" = "امنح الوصول إلى الموقع حتى يتمكن Element X من مشاركة موقعك.";
|
||||
"NSMicrophoneUsageDescription" = "يحتاج Element X إلى الوصول إلى الميكروفون لتسجيل الرسائل الصوتية وإرسالها.";
|
||||
"NSPhotoLibraryUsageDescription" = "يتيح لك هذا حفظ الصور ومقاطع الفيديو في مكتبة الصور لديك.";
|
||||
@@ -0,0 +1,64 @@
|
||||
"aeroplane" = "طَائِرة";
|
||||
"anchor" = "مِرسَاة";
|
||||
"apple" = "تُفَّاحَة";
|
||||
"ball" = "كُرَة";
|
||||
"banana" = "مَوزَة";
|
||||
"bell" = "جَرَس";
|
||||
"bicycle" = "دَرّاجَة";
|
||||
"book" = "كِتَاب";
|
||||
"butterfly" = "فَرَاشَة";
|
||||
"cactus" = "صبار";
|
||||
"cake" = "كَعكَة";
|
||||
"cat" = "هِرَّة";
|
||||
"clock" = "سَاعَة";
|
||||
"cloud" = "سَحابَة";
|
||||
"corn" = "ذُرَة";
|
||||
"dog" = "كلب";
|
||||
"elephant" = "فِيل";
|
||||
"fire" = "نار";
|
||||
"fish" = "سَمَكة";
|
||||
"flag" = "عَلَم";
|
||||
"flower" = "زَهرَة";
|
||||
"folder" = "مُجَلَّد";
|
||||
"gift" = "هَدِيَّة";
|
||||
"glasses" = "نَظَّارَة";
|
||||
"globe" = "كُرَةٌ أرضِيَّة";
|
||||
"guitar" = "غيتار";
|
||||
"hammer" = "مِطرَقَة";
|
||||
"hat" = "قُبَّعَة";
|
||||
"headphones" = "سَمّاعَة رَأس";
|
||||
"heart" = "قَلب";
|
||||
"horse" = "حِصَان";
|
||||
"hourglass" = "سَاعَةٌ رَملِيَّة";
|
||||
"key" = "مِفتَاح";
|
||||
"light_bulb" = "مِصبَاح";
|
||||
"lion" = "أَسَد";
|
||||
"lock" = "قُفل";
|
||||
"moon" = "قَمَر";
|
||||
"mushroom" = "فُطر";
|
||||
"octopus" = "أُخطُبُوط";
|
||||
"panda" = "باندَا";
|
||||
"paperclip" = "مِشبَكُ وَرَق";
|
||||
"pencil" = "قَلَمُ رَصاص";
|
||||
"penguin" = "بطريق";
|
||||
"pig" = "خِنزِير";
|
||||
"pin" = "دَبُّوس";
|
||||
"pizza" = "بِيتزا";
|
||||
"rabbit" = "أَرنَب";
|
||||
"robot" = "رُوبُوت";
|
||||
"rocket" = "صَارُوخ";
|
||||
"rooster" = "دِيك";
|
||||
"santa" = "سانتا";
|
||||
"scissors" = "مِقَصّ";
|
||||
"smiley" = "اِبتِسَامَة";
|
||||
"spanner" = "مِفتَاحُ رَبط";
|
||||
"strawberry" = "فَراوِلَة";
|
||||
"telephone" = "تِلِفُون";
|
||||
"thumbs_up" = "رَفعُ إِبهَام";
|
||||
"train" = "قِطَار";
|
||||
"tree" = "شَجَرَة";
|
||||
"trophy" = "كَأسُ النَّصر";
|
||||
"trumpet" = "بُوق";
|
||||
"turtle" = "سُلحفاة";
|
||||
"umbrella" = "مِظَلَّة";
|
||||
"unicorn" = "حصان وحيد القرن";
|
||||
@@ -1,4 +1,4 @@
|
||||
"a11y_add_avatar" = "Add avatar";
|
||||
"a11y_add_avatar" = "Дадаць аватар";
|
||||
"a11y_add_reaction" = "Дадаць рэакцыю: %1$@";
|
||||
"a11y_address" = "Адрас";
|
||||
"a11y_avatar" = "Аватар";
|
||||
@@ -6,16 +6,22 @@
|
||||
"a11y_collapse_message_text_field" = "Minimise message text field";
|
||||
"a11y_create_poll_votes_allowed_decrease" = "Decrease votes allowed per person";
|
||||
"a11y_create_poll_votes_allowed_increase" = "Increase votes allowed per person";
|
||||
"a11y_custom_emoji" = "Custom emoji";
|
||||
"a11y_delete" = "Выдаліць";
|
||||
"a11y_duration" = "Duration: %1$@";
|
||||
"a11y_duration" = "Працягласць: %1$@";
|
||||
"a11y_edit_avatar" = "Змяніць аватар";
|
||||
"a11y_edit_room_address_hint" = "Поўны адрас будзе %1$@";
|
||||
"a11y_expand_message_text_field" = "Expand message text field";
|
||||
"a11y_gallery_attachment" = "Gallery, attachment %1$d";
|
||||
"a11y_gallery_failed_scan" = "Gallery, failed scan %1$d";
|
||||
"a11y_gallery_image" = "Gallery, image %1$d";
|
||||
"a11y_gallery_scanning" = "Gallery, scanning %1$d";
|
||||
"a11y_gallery_video" = "Gallery, video %1$d";
|
||||
"a11y_hide_password" = "Схаваць пароль";
|
||||
"a11y_info" = "Інфармацыя";
|
||||
"a11y_join_call" = "Далучыцца да выкліку";
|
||||
"a11y_jump_to_bottom" = "Перайсці ўніз";
|
||||
"a11y_jump_to_unread" = "Jump to unread";
|
||||
"a11y_jump_to_unread" = "Перайсці да непрачытаных";
|
||||
"a11y_jump_to_unread_messages" = "Jump to first unread message";
|
||||
"a11y_notifications_mentions_only" = "Толькі згадкі";
|
||||
"a11y_notifications_muted" = "Гук адключаны";
|
||||
@@ -34,7 +40,7 @@
|
||||
"a11y_poll_end" = "Апытанне скончана";
|
||||
"a11y_polls_will_remove_selection" = "Will remove previous selection";
|
||||
"a11y_polls_winning_answer" = "This is the winning answer";
|
||||
"a11y_position" = "Position: %1$@";
|
||||
"a11y_position" = "Пазіцыя: %1$@";
|
||||
"a11y_qr_code" = "QR-код";
|
||||
"a11y_react_with" = "Рэагаваць з %1$@";
|
||||
"a11y_react_with_other_emojis" = "Рэагаваць з іншымі эмодзі";
|
||||
@@ -51,6 +57,7 @@
|
||||
"a11y_start_call" = "Пазваніць";
|
||||
"a11y_start_video_call" = "Start a video call";
|
||||
"a11y_start_voice_call" = "Start a voice call";
|
||||
"a11y_status" = "Статус";
|
||||
"a11y_tombstoned_room" = "Tombstoned room";
|
||||
"a11y_user_menu" = "Меню карыстальніка";
|
||||
"a11y_video_preview" = "Video preview";
|
||||
@@ -76,7 +83,7 @@
|
||||
"action_call" = "Званок";
|
||||
"action_cancel" = "Скасаваць";
|
||||
"action_cancel_for_now" = "Адмяніць пакуль";
|
||||
"action_choose_file" = "Choose file";
|
||||
"action_choose_file" = "Выбраць файл";
|
||||
"action_choose_photo" = "Выбраць фота";
|
||||
"action_clear" = "Ачысціць";
|
||||
"action_close" = "Закрыць";
|
||||
@@ -91,7 +98,7 @@
|
||||
"action_copy_text" = "Скапіяваць тэкст";
|
||||
"action_create" = "Стварыць";
|
||||
"action_create_room" = "Стварыце пакой";
|
||||
"action_create_space" = "Create space";
|
||||
"action_create_space" = "Стварыць прастору";
|
||||
"action_deactivate" = "Дэактываваць";
|
||||
"action_deactivate_account" = "Дэактываваць уліковы запіс";
|
||||
"action_decline" = "Адхіліць";
|
||||
@@ -136,14 +143,14 @@
|
||||
"action_manage_account_and_devices" = "Manage account & devices";
|
||||
"action_manage_devices" = "Кіраванне прыладамі";
|
||||
"action_manage_rooms" = "Manage rooms";
|
||||
"action_mark_as_read" = "Mark as read";
|
||||
"action_mark_as_read" = "Пазначыць як прачытанае";
|
||||
"action_message" = "Паведамленне";
|
||||
"action_minimize" = "Minimise";
|
||||
"action_next" = "Далей";
|
||||
"action_no" = "Не";
|
||||
"action_not_now" = "Не зараз";
|
||||
"action_ok" = "Добра";
|
||||
"action_open_context_menu" = "Open context menu";
|
||||
"action_open_context_menu" = "Адкрыць кантэкстнае меню";
|
||||
"action_open_settings" = "Налады";
|
||||
"action_open_with" = "Адкрыць з дапамогай";
|
||||
"action_pin" = "Замацаваць";
|
||||
@@ -167,6 +174,7 @@
|
||||
"action_retry_decryption" = "Паўтарыць расшыфроўку";
|
||||
"action_save" = "Захаваць";
|
||||
"action_search" = "Пошук";
|
||||
"action_select" = "Select";
|
||||
"action_select_all" = "Выбраць усё";
|
||||
"action_send" = "Адправіць";
|
||||
"action_send_edited_message" = "Send edited message";
|
||||
@@ -211,7 +219,7 @@
|
||||
"call_invalid_audio_device_bluetooth_devices_disabled" = "Element Call does not support using Bluetooth audio devices in this Android version. Please select a different audio device.";
|
||||
"common_about" = "Аб праграме";
|
||||
"common_acceptable_use_policy" = "Палітыка дапушчальнага выкарыстання";
|
||||
"common_add_account" = "Add an account";
|
||||
"common_add_account" = "Дадаць уліковы запіс";
|
||||
"common_add_another_account" = "Add account";
|
||||
"common_adding_caption" = "Adding caption";
|
||||
"common_advanced_settings" = "Пашыраныя налады";
|
||||
@@ -232,15 +240,15 @@
|
||||
"common_copied_to_clipboard" = "Скапіравана ў буфер абмену";
|
||||
"common_copyright" = "Аўтарскае права";
|
||||
"common_creating_room" = "Стварэнне пакоя…";
|
||||
"common_creating_space" = "Creating space…";
|
||||
"common_creating_space" = "Стварэнне прасторы…";
|
||||
"common_current_user_canceled_knock" = "Request canceled";
|
||||
"common_current_user_left_room" = "Выйшаў з пакоя";
|
||||
"common_current_user_left_space" = "Left space";
|
||||
"common_current_user_rejected_invite" = "Invite declined";
|
||||
"common_date_date_at_time" = "%1$@ at %2$@";
|
||||
"common_date_date_at_time" = "%1$@ у %2$@";
|
||||
"common_date_this_month" = "This month";
|
||||
"common_decryption_error" = "Памылка расшыфроўкі";
|
||||
"common_description" = "Description";
|
||||
"common_description" = "Апісанне";
|
||||
"common_developer_options" = "Параметры распрацоўшчыка";
|
||||
"common_device_id" = "Device ID";
|
||||
"common_direct_chat" = "Прамы чат";
|
||||
@@ -251,7 +259,7 @@
|
||||
"common_editing" = "Рэдагаванне";
|
||||
"common_editing_caption" = "Editing caption";
|
||||
"common_emote" = "* %1$@ %2$@";
|
||||
"common_empty_file" = "Empty file";
|
||||
"common_empty_file" = "Пусты файл";
|
||||
"common_encryption" = "Encryption";
|
||||
"common_encryption_enabled" = "Шыфраванне ўключана";
|
||||
"common_ends_at" = "Ends at %1$@";
|
||||
@@ -263,11 +271,11 @@
|
||||
"common_favourite" = "Абраць";
|
||||
"common_favourited" = "Абранае";
|
||||
"common_file" = "Файл";
|
||||
"common_file_deleted" = "File deleted";
|
||||
"common_file_saved" = "File saved";
|
||||
"common_file_deleted" = "Файл выдалены";
|
||||
"common_file_saved" = "Файл захаваны";
|
||||
"common_forward_message" = "Перасылка паведамлення";
|
||||
"common_frequently_used" = "Frequently used";
|
||||
"common_gallery" = "Gallery";
|
||||
"common_gallery" = "Галерэя";
|
||||
"common_gif" = "GIF";
|
||||
"common_group_call_in_progress" = "Group call in progress";
|
||||
"common_image" = "Відарыс";
|
||||
@@ -331,7 +339,7 @@
|
||||
"common_report_a_problem" = "Паведаміць аб праблеме";
|
||||
"common_report_submitted" = "Скарга прынята";
|
||||
"common_rich_text_editor" = "Рэдактар фарматаванага тэксту";
|
||||
"common_role" = "Role";
|
||||
"common_role" = "Роля";
|
||||
"common_room" = "Пакой";
|
||||
"common_room_name" = "Назва пакоя";
|
||||
"common_room_name_placeholder" = "напрыклад, назва вашага праекта";
|
||||
@@ -387,13 +395,6 @@
|
||||
"common_unsupported_call" = "Unsupported call";
|
||||
"common_unsupported_event" = "Падзея не падтрымліваецца";
|
||||
"common_user_started_a_call" = "%1$@ started a call";
|
||||
"common_user_status_away" = "Away";
|
||||
"common_user_status_be_right_back" = "Be right back";
|
||||
"common_user_status_custom" = "Custom…";
|
||||
"common_user_status_focus_time" = "Focus time";
|
||||
"common_user_status_in_a_meeting" = "In a meeting";
|
||||
"common_user_status_on_a_call" = "On a call";
|
||||
"common_user_status_on_the_road" = "On the road";
|
||||
"common_username" = "Імя карыстальніка";
|
||||
"common_verification_cancelled" = "Праверка адменена";
|
||||
"common_verification_complete" = "Праверка завершана";
|
||||
@@ -403,11 +404,11 @@
|
||||
"common_verify_identity" = "Verify identity";
|
||||
"common_verify_user" = "Verify user";
|
||||
"common_video" = "Відэа";
|
||||
"common_video_quality_high" = "High quality";
|
||||
"common_video_quality_high" = "Высокая якасць";
|
||||
"common_video_quality_high_description" = "Best quality but larger file size";
|
||||
"common_video_quality_low" = "Low quality";
|
||||
"common_video_quality_low" = "Нізкая якасць";
|
||||
"common_video_quality_low_description" = "Fastest upload speed and smallest file size";
|
||||
"common_video_quality_standard" = "Standard quality";
|
||||
"common_video_quality_standard" = "Стандартная якасць";
|
||||
"common_video_quality_standard_description" = "Balance of quality and upload speed";
|
||||
"common_voice_message" = "Галасавое паведамленне";
|
||||
"common_waiting" = "Чакаем…";
|
||||
@@ -420,7 +421,7 @@
|
||||
"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?";
|
||||
"confirm_recovery_key_banner_title" = "Ваша сховішча ключоў не сінхранізавана";
|
||||
"content_scanner_not_found" = "Download not available.";
|
||||
"content_scanner_not_found_title" = "File not found";
|
||||
"content_scanner_not_found_title" = "Something went wrong";
|
||||
"content_scanner_scanning" = "Scanning…";
|
||||
"content_scanner_unsafe_message" = "Preview and download have been disabled.";
|
||||
"content_scanner_unsafe_title" = "The file is not safe";
|
||||
@@ -444,7 +445,7 @@
|
||||
"crypto_identity_change_profile_pin_violation" = "%1$@’s digital identity was reset.";
|
||||
"crypto_identity_change_verification_violation_new" = "%1$@’s %2$@ digital identity was reset. %3$@";
|
||||
"crypto_identity_change_withdraw_verification_action" = "Withdraw verification";
|
||||
"dialog_allow_access" = "Allow access";
|
||||
"dialog_allow_access" = "Дазволіць доступ";
|
||||
"dialog_confirm_link_message" = "The link %1$@ is taking you to another site %2$@\n\nAre you sure you want to continue?";
|
||||
"dialog_confirm_link_title" = "Double-check this link";
|
||||
"dialog_default_video_quality_selector_subtitle" = "Select the default quality of videos you upload.";
|
||||
@@ -495,6 +496,11 @@
|
||||
"error_no_compatible_app_found" = "Не знойдзена сумяшчальная праграма для выканання гэтага дзеяння.";
|
||||
"error_room_address_already_exists" = "This room address already exists. Please try editing the room address field or change the room name";
|
||||
"error_room_address_invalid_symbols" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _";
|
||||
"error_sending_failure_invalid_mime_type" = "Invalid mime type: %@";
|
||||
"error_sending_failure_missing_media_content" = "Missing media content";
|
||||
"error_sending_failure_other" = "Error details: %@";
|
||||
"error_sending_failure_sending_from_unverified_device" = "Sending from unverified device";
|
||||
"error_sending_failure_unknown" = "Unknown error";
|
||||
"error_some_messages_have_not_been_sent" = "Некаторыя паведамленні не былі адпраўлены";
|
||||
"error_unknown" = "Выбачце, адбылася памылка";
|
||||
"error.account_already_logged_in" = "You're already logged in on this device as %1$@.";
|
||||
@@ -524,6 +530,8 @@
|
||||
"notification_invitation_action_reject" = "Адхіліць";
|
||||
"notification_invite_body" = "Запрасіў(-ла) вас у чат";
|
||||
"notification_invite_body_with_sender" = "%1$@ запрасіў(-ла) вас у чат";
|
||||
"notification_knock_request_body" = "Requested to join";
|
||||
"notification_live_location_started_body" = "Started sharing their live location";
|
||||
"notification_mentioned_you_body" = "Згадаў(-ла) вас: %1$@";
|
||||
"notification_new_messages" = "Новыя паведамленні";
|
||||
"notification_reaction_body" = "Адрэагаваў(-ла) на %1$@";
|
||||
@@ -708,14 +716,22 @@
|
||||
"screen_change_server_error_element_pro_required_title" = "Element Pro required";
|
||||
"screen_change_server_error_invalid_homeserver" = "Нам не ўдалося звязацца з гэтым хатнім серверам. Упэўніцеся, што вы правільна ўвялі URL-адрас хатняга сервера. Калі URL-адрас пазначаны правільна, звярніцеся да адміністратара хатняга сервера за дадатковай дапамогай.";
|
||||
"screen_change_server_error_invalid_well_known" = "Sliding sync недаступны з-за праблемы ў вядомым файле:\n%1$@";
|
||||
"screen_change_server_error_no_sliding_sync_message" = "The selected account provider does not support sliding sync. An upgrade to the server is needed to use %1$@.";
|
||||
"screen_change_server_error_no_sliding_sync_message" = "The selected account provider does not support sliding sync. The server needs to be upgraded to use %1$@.";
|
||||
"screen_change_server_error_unauthorized_homeserver" = "%1$@ is not allowed to connect to %2$@.";
|
||||
"screen_change_server_error_unauthorized_homeserver_content" = "This app has been configured to allow: %1$@.";
|
||||
"screen_change_server_error_unauthorized_homeserver_title" = "Account provider %1$@ not allowed.";
|
||||
"screen_change_server_form_header" = "URL хатняга сервера";
|
||||
"screen_change_server_form_notice" = "Enter a domain address.";
|
||||
"screen_change_server_navigation_title_login" = "Sign in";
|
||||
"screen_change_server_navigation_title_register" = "Create account";
|
||||
"screen_change_server_subtitle" = "Які адрас вашага сервера?";
|
||||
"screen_change_server_textfield_footer_login" = "If your account is provided by your company or a community, then change the account provider (example: companyname.com).";
|
||||
"screen_change_server_textfield_footer_register" = "Create an account on matrix.org for free. If you want to create an account with a different provider, then change it accordingly (example: communityname.org).";
|
||||
"screen_change_server_textfield_header" = "Account provider";
|
||||
"screen_change_server_textfield_placeholder" = "example.com";
|
||||
"screen_change_server_title" = "Выберыце свой сервер";
|
||||
"screen_change_server_title_login" = "Enter your account provider";
|
||||
"screen_change_server_title_register" = "Choose an account provider";
|
||||
"screen_chat_backup_key_backup_action_disable" = "Выключыць рэзервовае капіраванне";
|
||||
"screen_chat_backup_key_backup_action_enable" = "Уключыце рэзервовае капіраванне";
|
||||
"screen_chat_backup_key_backup_description" = "Надзейна захоўвайце вашу крыптаграфічную ідэнтыфікацыю і ключы паведамленняў на серверы. Гэта дазволіць вам праглядаць гісторыю паведамленняў на любых новых прыладах.%1$@ .";
|
||||
@@ -762,7 +778,7 @@
|
||||
"screen_create_room_private_option_description" = "Толькі запрошаныя людзі могуць атрымаць доступ да гэтага пакоя. Усе паведамленні абаронены end-to-end шыфраваннем.";
|
||||
"screen_create_room_private_option_title" = "Private";
|
||||
"screen_create_room_public_option_description" = "Любы можа знайсці гэты пакой. \nВы можаце змяніць гэта ў любы час у наладах пакоя.";
|
||||
"screen_create_room_public_option_short_description" = "Anyone can join.";
|
||||
"screen_create_room_public_option_short_description" = "Anyone can join, including people outside your organisation.";
|
||||
"screen_create_room_public_option_title" = "Public";
|
||||
"screen_create_room_room_access_section_knocking_option_description" = "Anyone can ask to join but an administrator or a moderator must accept the request.";
|
||||
"screen_create_room_room_access_section_knocking_option_title" = "Папрасіце далучыцца";
|
||||
@@ -770,7 +786,7 @@
|
||||
"screen_create_room_room_access_section_knocking_restricted_option_title" = "Ask to join";
|
||||
"screen_create_room_room_access_section_private_option_description" = "Only people invited can join.";
|
||||
"screen_create_room_room_access_section_private_option_title" = "Private";
|
||||
"screen_create_room_room_access_section_public_option_description" = "Anyone can join.";
|
||||
"screen_create_room_room_access_section_public_option_description" = "Anyone can join, including people outside your organisation.";
|
||||
"screen_create_room_room_access_section_public_option_title" = "Public";
|
||||
"screen_create_room_room_access_section_restricted_option_description" = "Anyone in %1$@ can join.";
|
||||
"screen_create_room_room_access_section_restricted_option_title" = "Standard";
|
||||
@@ -935,6 +951,10 @@
|
||||
"screen_knock_requests_list_empty_state_title" = "No pending request to join";
|
||||
"screen_knock_requests_list_initial_loading_title" = "Loading requests to join…";
|
||||
"screen_knock_requests_list_title" = "Requests to join";
|
||||
"screen_labs_enable_gallery" = "Gallery messages";
|
||||
"screen_labs_enable_gallery_description" = "Send multiple images or videos in one go. Stable on mobile, but not yet supported by web.";
|
||||
"screen_labs_enable_knocking" = "Ask to join";
|
||||
"screen_labs_enable_knocking_description" = "Users can request access to a room that is not open for anyone to join. Stable on mobile, web implementation exists but is experimental.";
|
||||
"screen_labs_enable_threads" = "Enable thread replies";
|
||||
"screen_labs_enable_threads_description" = "The app will restart to apply this change.";
|
||||
"screen_labs_header_description" = "Try out our latest ideas in development. These features are not finalised; they may be unstable, may change.";
|
||||
@@ -943,11 +963,11 @@
|
||||
"screen_leave_space_choose_owners_action" = "Choose owners";
|
||||
"screen_leave_space_last_admin_info" = "%1$@ (Admin)";
|
||||
"screen_leave_space_subtitle" = "Select the rooms you’d like to leave which you're not the only administrator for:";
|
||||
"screen_leave_space_subtitle_last_admin" = "You need to assign another admin for this space before you can leave.";
|
||||
"screen_leave_space_subtitle_last_admin" = "You need to assign another administrator for this space before you can leave.";
|
||||
"screen_leave_space_subtitle_last_owner" = "You are the only owner of %1$@. You need to transfer ownership to someone else before you leave.";
|
||||
"screen_leave_space_subtitle_only_last_admin" = "You will not be removed from the following room(s) because you're the only administrator:";
|
||||
"screen_leave_space_title" = "Leave %1$@?";
|
||||
"screen_leave_space_title_last_admin" = "You are the only admin for %1$@";
|
||||
"screen_leave_space_title_last_admin" = "You are the only administrator for %1$@";
|
||||
"screen_leave_space_title_last_owner" = "Transfer ownership";
|
||||
"screen_link_new_device_authentication_reason_ios" = "Confirm it's you before linking a new device.";
|
||||
"screen_link_new_device_desktop_scanning_title" = "Сканіраваць QR-код";
|
||||
@@ -955,6 +975,7 @@
|
||||
"screen_link_new_device_desktop_step3" = "Адсканіруйце QR-код з дапамогай гэтай прылады";
|
||||
"screen_link_new_device_desktop_submit" = "Гатовы да сканіравання";
|
||||
"screen_link_new_device_desktop_title" = "Open %1$@ on a desktop computer to get the QR code";
|
||||
"screen_link_new_device_desktop_warning" = "Only scan a QR code after following the steps above on your own device. Never scan a code sent by another person.";
|
||||
"screen_link_new_device_enter_number_error_numbers_do_not_match" = "The numbers don’t match";
|
||||
"screen_link_new_device_enter_number_notice" = "Enter 2-digit code";
|
||||
"screen_link_new_device_enter_number_subtitle" = "This will verify that the connection to your other device is secure.";
|
||||
@@ -981,7 +1002,7 @@
|
||||
"screen_live_location_sheet_nobody_sharing" = "Nobody is sharing their location";
|
||||
"screen_live_location_sheet_sharing_live_location" = "Sharing live location";
|
||||
"screen_live_location_sheet_title" = "On the map";
|
||||
"screen_local_network_opt_in_subtitle" = "Your homeserver is on your local network. To connect, Element needs permission to reach devices on this network.";
|
||||
"screen_local_network_opt_in_subtitle" = "Your homeserver is on your local network. To connect, %1$@ needs permission to reach devices on this network.";
|
||||
"screen_local_network_opt_in_title" = "Allow access to local network";
|
||||
"screen_login_error_deactivated_account" = "Гэты ўліковы запіс быў выдалены.";
|
||||
"screen_login_error_invalid_credentials" = "Няправільнае імя карыстальніка і/або пароль";
|
||||
@@ -1033,7 +1054,7 @@
|
||||
"screen_missing_key_backup_open_element_classic" = "Open Element Classic";
|
||||
"screen_missing_key_backup_step_1" = "Open Element Classic on your device";
|
||||
"screen_missing_key_backup_step_2_ios" = "Go to User settings > Security";
|
||||
"screen_missing_key_backup_step_3_ios" = "Scroll down to Secure Backup and tap on Set up";
|
||||
"screen_missing_key_backup_step_3_ios" = "Scroll down to Chat Backup and tap on Set up";
|
||||
"screen_missing_key_backup_step_4" = "Follow the instructions to enable your key storage";
|
||||
"screen_missing_key_backup_step_5" = "Come back to %1$@";
|
||||
"screen_missing_key_backup_title" = "Enable your key storage before proceeding to %1$@";
|
||||
@@ -1125,8 +1146,6 @@
|
||||
"screen_polls_history_filter_ongoing" = "Бягучыя";
|
||||
"screen_polls_history_filter_past" = "Мінулыя";
|
||||
"screen_polls_history_title" = "Апытанні";
|
||||
"screen_preferences_user_status_custom_hint" = "Custom status…";
|
||||
"screen_preferences_user_status_placeholder" = "What's your status?";
|
||||
"screen_push_history_title" = "Push history";
|
||||
"screen_qr_code_login_connecting_subtitle" = "Ўсталяванне бяспечнага злучэння";
|
||||
"screen_qr_code_login_connection_note_secure_state_description" = "Не атрымалася ўсталяваць бяспечнае злучэнне з новай прыладай. Існуючыя прылады па-ранейшаму ў бяспецы, і вам не трэба турбавацца пра іх.";
|
||||
@@ -1204,7 +1223,7 @@
|
||||
"screen_report_content_hint" = "Прычына, па якой вы паскардзіліся на гэты змест";
|
||||
"screen_report_room_leave_failed_alert_message" = "Your report was submitted successfully, but we encountered an issue while trying to leave the room. Please try again.";
|
||||
"screen_report_room_leave_failed_alert_title" = "Unable to Leave Room";
|
||||
"screen_report_room_reason_footer" = "Report this room to your admin. If the messages are encrypted, your admin will not be able to read them.";
|
||||
"screen_report_room_reason_footer" = "Report this chat to your account provider. If the messages are encrypted, your account provider will not be able to read them.";
|
||||
"screen_report_room_reason_placeholder" = "Describe the reason to report…";
|
||||
"screen_report_room_title" = "Report room";
|
||||
"screen_reset_encryption_confirmation_alert_action" = "Так, скінуць зараз";
|
||||
@@ -1271,6 +1290,10 @@
|
||||
"screen_room_change_role_section_users" = "Удзельнікі";
|
||||
"screen_room_change_role_unsaved_changes_description" = "У вас ёсць незахаваныя змены.";
|
||||
"screen_room_change_role_unsaved_changes_title" = "Захаваць змены?";
|
||||
"screen_room_confirm_removal_message" = "Are you sure you wish to remove this message?";
|
||||
"screen_room_confirm_removal_reason_label" = "Reason (optional)";
|
||||
"screen_room_confirm_removal_reason_placeholder" = "Enter…";
|
||||
"screen_room_confirm_removal_title" = "Confirm removal";
|
||||
"screen_room_details_add_topic_title" = "Дадаць тэму";
|
||||
"screen_room_details_badge_encrypted" = "Зашыфраваны";
|
||||
"screen_room_details_badge_not_encrypted" = "Не зашыфраваны";
|
||||
@@ -1316,6 +1339,7 @@
|
||||
"screen_room_invite_again_alert_message" = "Вы хочаце запрасіць іх назад?";
|
||||
"screen_room_invite_again_alert_title" = "Вы адзін у гэтым чаце";
|
||||
"screen_room_live_location_banner" = "Sharing live location";
|
||||
"screen_room_maximum_messages_selected" = "Maximum messages selected";
|
||||
"screen_room_member_details_block_alert_action" = "Заблакіраваць";
|
||||
"screen_room_member_details_block_alert_description" = "Заблакіраваныя карыстальнікі не змогуць адпраўляць вам паведамленні, і ўсе іх паведамленні будуць схаваны. Вы можаце разблакіраваць іх у любы час.";
|
||||
"screen_room_member_details_block_user" = "Заблакіраваць карыстальніка";
|
||||
@@ -1356,7 +1380,8 @@
|
||||
"screen_room_notification_settings_error_setting_mode" = "Не ўдалося наладзіць рэжым, паспрабуйце яшчэ раз.";
|
||||
"screen_room_notification_settings_mentions_only_disclaimer" = "Ваш хатні сервер не падтрымлівае гэту опцыю ў зашыфраваных пакоях, вы не атрымаеце апавяшчэнне ў гэтым пакоі.";
|
||||
"screen_room_notification_settings_mode_all_messages" = "Усе паведамленні";
|
||||
"screen_room_notification_settings_mode_mentions_and_keywords" = "Толькі згадванні і ключавыя словы";
|
||||
"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and replies only";
|
||||
"screen_room_notification_settings_mode_mute" = "Mute";
|
||||
"screen_room_notification_settings_room_custom_settings_title" = "У гэтым пакоі паведаміце мяне пра";
|
||||
"screen_room_pinned_banner_indicator" = "%1$@ з %2$@";
|
||||
"screen_room_pinned_banner_indicator_description" = "%1$@ Замацаваныя паведамленні";
|
||||
@@ -1417,6 +1442,8 @@
|
||||
"screen_roomlist_filter_invites_empty_state_title" = "У вас няма непрынятых запрашэнняў.";
|
||||
"screen_roomlist_filter_low_priority" = "Нізкі прыярытэт";
|
||||
"screen_roomlist_filter_low_priority_empty_state_title" = "You don’t have any low priority chats yet";
|
||||
"screen_roomlist_filter_mention" = "Mention";
|
||||
"screen_roomlist_filter_mention_empty_state_title" = "You don’t have any unread mentions";
|
||||
"screen_roomlist_filter_mixed_empty_state_subtitle" = "Вы можаце прыбраць фільтры, каб убачыць іншыя вашыя чаты.";
|
||||
"screen_roomlist_filter_mixed_empty_state_title" = "У вас няма чатаў для гэтай катэгорыі";
|
||||
"screen_roomlist_filter_people" = "Людзі";
|
||||
@@ -1442,7 +1469,7 @@
|
||||
"screen_security_and_privacy_encryption_section_footer" = "Once enabled, encryption cannot be disabled.";
|
||||
"screen_security_and_privacy_encryption_section_header" = "Encryption";
|
||||
"screen_security_and_privacy_encryption_toggle_title" = "Enable end-to-end encryption";
|
||||
"screen_security_and_privacy_room_access_anyone_option_description" = "Anyone can join.";
|
||||
"screen_security_and_privacy_room_access_anyone_option_description" = "Anyone can join, including people outside your organisation.";
|
||||
"screen_security_and_privacy_room_access_anyone_option_title" = "Anyone";
|
||||
"screen_security_and_privacy_room_access_footer" = "Choose which spaces’ members can join this room without an invitation. %1$@";
|
||||
"screen_security_and_privacy_room_access_footer_manage_spaces_action" = "Manage spaces";
|
||||
@@ -1503,6 +1530,8 @@
|
||||
"screen_session_verification_request_title" = "Verification requested";
|
||||
"screen_session_verification_they_dont_match" = "Яны не супадаюць";
|
||||
"screen_session_verification_they_match" = "Яны супадаюць";
|
||||
"screen_session_verification_unavailable_subtitle" = "Please use another device or recovery key.";
|
||||
"screen_session_verification_unavailable_title" = "Verification with this device is currently unavailable";
|
||||
"screen_session_verification_use_another_device_subtitle" = "Make sure you have the app open in the other device before starting verification from here.";
|
||||
"screen_session_verification_use_another_device_title" = "Open the app on another verified device";
|
||||
"screen_session_verification_user_initiator_subtitle" = "For extra security, verify this user by comparing a set of emojis on your devices. Do this by using a trusted way to communicate.";
|
||||
@@ -1515,6 +1544,14 @@
|
||||
"screen_session_verification_waiting_subtitle" = "Once accepted you’ll be able to continue with the verification.";
|
||||
"screen_session_verification_waiting_to_accept_subtitle" = "Для працягу працы прыміце запыт на запуск працэсу праверкі ў іншым сеансе.";
|
||||
"screen_session_verification_waiting_to_accept_title" = "Чаканне прыняцця запыту";
|
||||
"screen_settings_user_status_away" = "Away";
|
||||
"screen_settings_user_status_be_right_back" = "Be right back";
|
||||
"screen_settings_user_status_custom" = "Custom…";
|
||||
"screen_settings_user_status_custom_hint" = "Custom status…";
|
||||
"screen_settings_user_status_focus_time" = "Focus time";
|
||||
"screen_settings_user_status_in_a_meeting" = "In a meeting";
|
||||
"screen_settings_user_status_on_the_road" = "On the road";
|
||||
"screen_settings_user_status_placeholder" = "What's your status?";
|
||||
"screen_share_location_live_location_disclaimer_title" = "Your live location history will be stored in the room and visible to members after the session ends.";
|
||||
"screen_share_location_live_location_duration_picker_title" = "Choose how long to share your live location.";
|
||||
"screen_share_location_live_location_missing_permissions" = "You do not have permissions to share your live location in this room";
|
||||
@@ -1554,7 +1591,7 @@
|
||||
"screen_space_list_parent_space" = "%1$@ space";
|
||||
"screen_space_list_title" = "Прасторы";
|
||||
"screen_space_menu_action_members" = "View members";
|
||||
"screen_space_remove_rooms_confirmation_content" = "Removing a room will not affect the room access. To change the access go to Room info > Privacy & security.";
|
||||
"screen_space_remove_rooms_confirmation_content" = "Removing a room will not affect the room access. To change the access go to Room info > Security & privacy.";
|
||||
"screen_space_remove_rooms_confirmation_title_ios" = "Remove rooms from %1$@?";
|
||||
"screen_space_settings_leave_space" = "Пакінуць прастору";
|
||||
"screen_space_settings_roles_and_permissions" = "Ролі і дазволы";
|
||||
@@ -1705,6 +1742,7 @@
|
||||
"troubleshoot_notifications_test_push_loop_back_failure_2" = "Памылка: %1$@.";
|
||||
"troubleshoot_notifications_test_push_loop_back_failure_3" = "Памылка, немагчыма праверыць push-апавяшчэнне.";
|
||||
"troubleshoot_notifications_test_push_loop_back_failure_4" = "Памылка, тайм-аўт у чаканні push-апавяшчэння.";
|
||||
"troubleshoot_notifications_test_push_loop_back_failure_rate_limit" = "Error: the push gateway is rate limiting this device, so notifications won't be reliable. Please set up a different push gateway.";
|
||||
"troubleshoot_notifications_test_push_loop_back_success" = "Зварот цыклу назад заняў %1$d мс.";
|
||||
"troubleshoot_notifications_test_push_loop_back_title" = "Тэст Націсніце кнопку вярнуцца";
|
||||
"troubleshoot_notifications_test_unified_push_description" = "Пераканайцеся, што размеркавальнікі UnifiedPush даступныя.";
|
||||
|
||||
@@ -20,6 +20,22 @@
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>a11y_gallery_more_media</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Gallery, %1$d more media</string>
|
||||
<key>other</key>
|
||||
<string>Gallery, %1$d more medias</string>
|
||||
</dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>a11y_polls_percent_of_total</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
@@ -256,6 +272,22 @@
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>notification_gallery_body</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Shared %1$d attachment</string>
|
||||
<key>other</key>
|
||||
<string>Shared %1$d attachments</string>
|
||||
</dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>notification_invitations</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
@@ -544,6 +576,22 @@
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>screen_room_selection_count</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%1$d message selected</string>
|
||||
<key>other</key>
|
||||
<string>%1$d messages selected</string>
|
||||
</dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>screen_room_timeline_reaction_a11y</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
|
||||
@@ -6,11 +6,17 @@
|
||||
"a11y_collapse_message_text_field" = "Minimise message text field";
|
||||
"a11y_create_poll_votes_allowed_decrease" = "Decrease votes allowed per person";
|
||||
"a11y_create_poll_votes_allowed_increase" = "Increase votes allowed per person";
|
||||
"a11y_custom_emoji" = "Custom emoji";
|
||||
"a11y_delete" = "Изтриване";
|
||||
"a11y_duration" = "Duration: %1$@";
|
||||
"a11y_edit_avatar" = "Edit avatar";
|
||||
"a11y_edit_room_address_hint" = "Пълният адрес ще бъде %1$@";
|
||||
"a11y_expand_message_text_field" = "Expand message text field";
|
||||
"a11y_gallery_attachment" = "Gallery, attachment %1$d";
|
||||
"a11y_gallery_failed_scan" = "Gallery, failed scan %1$d";
|
||||
"a11y_gallery_image" = "Gallery, image %1$d";
|
||||
"a11y_gallery_scanning" = "Gallery, scanning %1$d";
|
||||
"a11y_gallery_video" = "Gallery, video %1$d";
|
||||
"a11y_hide_password" = "Скриване на паролата";
|
||||
"a11y_info" = "Info";
|
||||
"a11y_join_call" = "Присъединяване към обаждане";
|
||||
@@ -51,6 +57,7 @@
|
||||
"a11y_start_call" = "Започнете обаждане";
|
||||
"a11y_start_video_call" = "Start a video call";
|
||||
"a11y_start_voice_call" = "Start a voice call";
|
||||
"a11y_status" = "Status";
|
||||
"a11y_tombstoned_room" = "Tombstoned room";
|
||||
"a11y_user_menu" = "Потребителско меню";
|
||||
"a11y_video_preview" = "Video preview";
|
||||
@@ -167,6 +174,7 @@
|
||||
"action_retry_decryption" = "Повторен опит за разшифроване";
|
||||
"action_save" = "Запазване";
|
||||
"action_search" = "Търсене";
|
||||
"action_select" = "Select";
|
||||
"action_select_all" = "Select all";
|
||||
"action_send" = "Изпращане";
|
||||
"action_send_edited_message" = "Send edited message";
|
||||
@@ -201,7 +209,7 @@
|
||||
"banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade";
|
||||
"banner_migrate_to_native_sliding_sync_app_force_logout_title" = "%1$@ no longer supports the old protocol. Please log out and log back in to continue using the app.";
|
||||
"banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later.";
|
||||
"banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app.";
|
||||
"banner_migrate_to_native_sliding_sync_force_logout_title" = "Your account provider no longer supports the old protocol. Please sign out and sign back in to continue using the app.";
|
||||
"banner_migrate_to_native_sliding_sync_title" = "Upgrade available";
|
||||
"banner_new_sound_message" = "Your notification ping has been updated—clearer, quicker, and less disruptive.";
|
||||
"banner_new_sound_title" = "We’ve refreshed your sounds";
|
||||
@@ -387,13 +395,6 @@
|
||||
"common_unsupported_call" = "Unsupported call";
|
||||
"common_unsupported_event" = "Неподдържано събитие";
|
||||
"common_user_started_a_call" = "%1$@ started a call";
|
||||
"common_user_status_away" = "Away";
|
||||
"common_user_status_be_right_back" = "Be right back";
|
||||
"common_user_status_custom" = "Custom…";
|
||||
"common_user_status_focus_time" = "Focus time";
|
||||
"common_user_status_in_a_meeting" = "In a meeting";
|
||||
"common_user_status_on_a_call" = "On a call";
|
||||
"common_user_status_on_the_road" = "On the road";
|
||||
"common_username" = "Потребителско име";
|
||||
"common_verification_cancelled" = "Потвърждаването е отменено";
|
||||
"common_verification_complete" = "Потвърждаването е завършено";
|
||||
@@ -420,7 +421,7 @@
|
||||
"confirm_recovery_key_banner_secondary_button_title" = "Forgot your recovery key?";
|
||||
"confirm_recovery_key_banner_title" = "Хранилището ви за ключове не е синхронизирано";
|
||||
"content_scanner_not_found" = "Download not available.";
|
||||
"content_scanner_not_found_title" = "File not found";
|
||||
"content_scanner_not_found_title" = "Something went wrong";
|
||||
"content_scanner_scanning" = "Scanning…";
|
||||
"content_scanner_unsafe_message" = "Preview and download have been disabled.";
|
||||
"content_scanner_unsafe_title" = "The file is not safe";
|
||||
@@ -483,7 +484,7 @@
|
||||
"emoji_picker_category_recent" = "Recent emojis";
|
||||
"emoji_picker_category_symbols" = "Символи";
|
||||
"emoji_picker_search_placeholder" = "Search emojis";
|
||||
"error_account_creation_not_possible" = "Your homeserver needs to be upgraded to support Matrix Authentication Service and account creation.";
|
||||
"error_account_creation_not_possible" = "Your account provider needs to be upgraded to support Matrix Authentication Service and account creation.";
|
||||
"error_failed_creating_the_permalink" = "Неуспешно създаване на постоянна връзка";
|
||||
"error_failed_loading_map" = "%1$@ не успя да зареди картата. Моля, опитайте отново по-късно.";
|
||||
"error_failed_loading_messages" = "Неуспешно зареждане на съобщения";
|
||||
@@ -495,6 +496,11 @@
|
||||
"error_no_compatible_app_found" = "No compatible app was found to handle this action.";
|
||||
"error_room_address_already_exists" = "This room address already exists. Please try editing the room address field or change the room name";
|
||||
"error_room_address_invalid_symbols" = "Some characters are not allowed. Only letters, digits and the following symbols are supported ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _";
|
||||
"error_sending_failure_invalid_mime_type" = "Invalid mime type: %@";
|
||||
"error_sending_failure_missing_media_content" = "Missing media content";
|
||||
"error_sending_failure_other" = "Error details: %@";
|
||||
"error_sending_failure_sending_from_unverified_device" = "Sending from unverified device";
|
||||
"error_sending_failure_unknown" = "Unknown error";
|
||||
"error_some_messages_have_not_been_sent" = "Някои съобщения не са изпратени";
|
||||
"error_unknown" = "Съжаляваме, възникна грешка";
|
||||
"error.account_already_logged_in" = "You're already logged in on this device as %1$@.";
|
||||
@@ -524,6 +530,8 @@
|
||||
"notification_invitation_action_reject" = "Reject";
|
||||
"notification_invite_body" = "Поканиха ви за чат";
|
||||
"notification_invite_body_with_sender" = "%1$@ invited you to chat";
|
||||
"notification_knock_request_body" = "Requested to join";
|
||||
"notification_live_location_started_body" = "Started sharing their live location";
|
||||
"notification_mentioned_you_body" = "Ви спомена: %1$@";
|
||||
"notification_new_messages" = "Нови съобщения";
|
||||
"notification_reaction_body" = "Реагира с %1$@";
|
||||
@@ -708,14 +716,22 @@
|
||||
"screen_change_server_error_element_pro_required_title" = "Element Pro required";
|
||||
"screen_change_server_error_invalid_homeserver" = "Не можахме да достигнем този сървър. Моля, проверете дали сте въвели правилно URL адреса на сървъра. Ако URL адресът е правилен, свържете се с администратора на вашия сървър за допълнителна помощ.";
|
||||
"screen_change_server_error_invalid_well_known" = "Server isn't available due to an issue in the .well-known file:\n%1$@";
|
||||
"screen_change_server_error_no_sliding_sync_message" = "The selected account provider does not support sliding sync. An upgrade to the server is needed to use %1$@.";
|
||||
"screen_change_server_error_no_sliding_sync_message" = "The selected account provider does not support sliding sync. The server needs to be upgraded to use %1$@.";
|
||||
"screen_change_server_error_unauthorized_homeserver" = "%1$@ is not allowed to connect to %2$@.";
|
||||
"screen_change_server_error_unauthorized_homeserver_content" = "This app has been configured to allow: %1$@.";
|
||||
"screen_change_server_error_unauthorized_homeserver_title" = "Account provider %1$@ not allowed.";
|
||||
"screen_change_server_form_header" = "URL адрес на сървъра";
|
||||
"screen_change_server_form_notice" = "Enter a domain address.";
|
||||
"screen_change_server_navigation_title_login" = "Sign in";
|
||||
"screen_change_server_navigation_title_register" = "Create account";
|
||||
"screen_change_server_subtitle" = "Какъв е адресът на вашия сървър?";
|
||||
"screen_change_server_textfield_footer_login" = "If your account is provided by your company or a community, then change the account provider (example: companyname.com).";
|
||||
"screen_change_server_textfield_footer_register" = "Create an account on matrix.org for free. If you want to create an account with a different provider, then change it accordingly (example: communityname.org).";
|
||||
"screen_change_server_textfield_header" = "Account provider";
|
||||
"screen_change_server_textfield_placeholder" = "example.com";
|
||||
"screen_change_server_title" = "Изберете своя сървър";
|
||||
"screen_change_server_title_login" = "Enter your account provider";
|
||||
"screen_change_server_title_register" = "Choose an account provider";
|
||||
"screen_chat_backup_key_backup_action_disable" = "Изтриване на хранилището за ключове";
|
||||
"screen_chat_backup_key_backup_action_enable" = "Включване на резервните копия";
|
||||
"screen_chat_backup_key_backup_description" = "Съхранявайте сигурно криптографската си самоличност и ключовете за съобщения на сървъра. Това ще ви позволи да преглеждате историята на съобщенията си на всички нови устройства.%1$@ .";
|
||||
@@ -730,7 +746,7 @@
|
||||
"screen_chat_backup_recovery_action_setup" = "Get recovery key";
|
||||
"screen_chat_backup_recovery_action_setup_description" = "Your chats are automatically backed up with end-to-end encryption. To restore this backup and retain your digital identity when you lose access to all your devices, you will need your recovery key.";
|
||||
"screen_create_account_title" = "Създаване на акаунт";
|
||||
"screen_create_new_recovery_key_list_item_1" = "Open %1$@ in a desktop device";
|
||||
"screen_create_new_recovery_key_list_item_1" = "Open %1$@ on a desktop device";
|
||||
"screen_create_new_recovery_key_list_item_2" = "Sign into your account again";
|
||||
"screen_create_new_recovery_key_list_item_3" = "When asked to verify your device, select %1$@";
|
||||
"screen_create_new_recovery_key_list_item_3_reset_all" = "“Reset all”";
|
||||
@@ -762,7 +778,7 @@
|
||||
"screen_create_room_private_option_description" = "Само поканени хора имат достъп до тази стая. Всички съобщения са шифровани от край до край.";
|
||||
"screen_create_room_private_option_title" = "Private";
|
||||
"screen_create_room_public_option_description" = "Всеки може да намери тази стая.\nМожете да промените това по всяко време в настройките на стаята.";
|
||||
"screen_create_room_public_option_short_description" = "Anyone can join.";
|
||||
"screen_create_room_public_option_short_description" = "Anyone can join, including people outside your organisation.";
|
||||
"screen_create_room_public_option_title" = "Public";
|
||||
"screen_create_room_room_access_section_knocking_option_description" = "Anyone can ask to join but an administrator or a moderator must accept the request.";
|
||||
"screen_create_room_room_access_section_knocking_option_title" = "Allow ask to join";
|
||||
@@ -899,7 +915,7 @@
|
||||
"screen_join_room_loading_alert_title" = "We couldn’t display this room preview";
|
||||
"screen_join_room_space_not_supported_description" = "%1$@ does not support spaces yet. You can access spaces on web.";
|
||||
"screen_join_room_space_not_supported_title" = "Spaces are not supported yet";
|
||||
"screen_join_room_subtitle_knock" = "Click the button below and a room administrator will be notified. You’ll be able to join the conversation once approved.";
|
||||
"screen_join_room_subtitle_knock" = "Tap the button below and a chat administrator will be notified. You'll be able to join the conversation once approved.";
|
||||
"screen_join_room_subtitle_no_preview" = "You must be a member of this room to view the message history.";
|
||||
"screen_join_room_title_knock" = "Want to join this room?";
|
||||
"screen_join_room_title_no_preview" = "Preview is not available";
|
||||
@@ -935,6 +951,10 @@
|
||||
"screen_knock_requests_list_empty_state_title" = "No pending request to join";
|
||||
"screen_knock_requests_list_initial_loading_title" = "Loading requests to join…";
|
||||
"screen_knock_requests_list_title" = "Requests to join";
|
||||
"screen_labs_enable_gallery" = "Gallery messages";
|
||||
"screen_labs_enable_gallery_description" = "Send multiple images or videos in one go. Stable on mobile, but not yet supported by web.";
|
||||
"screen_labs_enable_knocking" = "Ask to join";
|
||||
"screen_labs_enable_knocking_description" = "Users can request access to a room that is not open for anyone to join. Stable on mobile, web implementation exists but is experimental.";
|
||||
"screen_labs_enable_threads" = "Включване на отговори в нишка";
|
||||
"screen_labs_enable_threads_description" = "The app will restart to apply this change.";
|
||||
"screen_labs_header_description" = "Try out our latest ideas in development. These features are not finalised; they may be unstable, may change.";
|
||||
@@ -943,11 +963,11 @@
|
||||
"screen_leave_space_choose_owners_action" = "Choose owners";
|
||||
"screen_leave_space_last_admin_info" = "%1$@ (Admin)";
|
||||
"screen_leave_space_subtitle" = "Select the rooms you’d like to leave which you're not the only administrator for:";
|
||||
"screen_leave_space_subtitle_last_admin" = "You need to assign another admin for this space before you can leave.";
|
||||
"screen_leave_space_subtitle_last_admin" = "You need to assign another administrator for this space before you can leave.";
|
||||
"screen_leave_space_subtitle_last_owner" = "You are the only owner of %1$@. You need to transfer ownership to someone else before you leave.";
|
||||
"screen_leave_space_subtitle_only_last_admin" = "You will not be removed from the following room(s) because you're the only administrator:";
|
||||
"screen_leave_space_title" = "Leave %1$@?";
|
||||
"screen_leave_space_title_last_admin" = "You are the only admin for %1$@";
|
||||
"screen_leave_space_title_last_admin" = "You are the only administrator for %1$@";
|
||||
"screen_leave_space_title_last_owner" = "Transfer ownership";
|
||||
"screen_link_new_device_authentication_reason_ios" = "Confirm it's you before linking a new device.";
|
||||
"screen_link_new_device_desktop_scanning_title" = "Scan the QR code";
|
||||
@@ -955,6 +975,7 @@
|
||||
"screen_link_new_device_desktop_step3" = "Scan the QR code with this device";
|
||||
"screen_link_new_device_desktop_submit" = "Ready to scan";
|
||||
"screen_link_new_device_desktop_title" = "Open %1$@ on a desktop computer to get the QR code";
|
||||
"screen_link_new_device_desktop_warning" = "Only scan a QR code after following the steps above on your own device. Never scan a code sent by another person.";
|
||||
"screen_link_new_device_enter_number_error_numbers_do_not_match" = "The numbers don’t match";
|
||||
"screen_link_new_device_enter_number_notice" = "Enter 2-digit code";
|
||||
"screen_link_new_device_enter_number_subtitle" = "This will verify that the connection to your other device is secure.";
|
||||
@@ -981,7 +1002,7 @@
|
||||
"screen_live_location_sheet_nobody_sharing" = "Nobody is sharing their location";
|
||||
"screen_live_location_sheet_sharing_live_location" = "Sharing live location";
|
||||
"screen_live_location_sheet_title" = "On the map";
|
||||
"screen_local_network_opt_in_subtitle" = "Your homeserver is on your local network. To connect, Element needs permission to reach devices on this network.";
|
||||
"screen_local_network_opt_in_subtitle" = "Your homeserver is on your local network. To connect, %1$@ needs permission to reach devices on this network.";
|
||||
"screen_local_network_opt_in_title" = "Allow access to local network";
|
||||
"screen_login_error_deactivated_account" = "Този акаунт бе деактивиран.";
|
||||
"screen_login_error_invalid_credentials" = "Неправилно потребителско име и/или парола";
|
||||
@@ -1033,7 +1054,7 @@
|
||||
"screen_missing_key_backup_open_element_classic" = "Open Element Classic";
|
||||
"screen_missing_key_backup_step_1" = "Open Element Classic on your device";
|
||||
"screen_missing_key_backup_step_2_ios" = "Go to User settings > Security";
|
||||
"screen_missing_key_backup_step_3_ios" = "Scroll down to Secure Backup and tap on Set up";
|
||||
"screen_missing_key_backup_step_3_ios" = "Scroll down to Chat Backup and tap on Set up";
|
||||
"screen_missing_key_backup_step_4" = "Follow the instructions to enable your key storage";
|
||||
"screen_missing_key_backup_step_5" = "Come back to %1$@";
|
||||
"screen_missing_key_backup_title" = "Enable your key storage before proceeding to %1$@";
|
||||
@@ -1125,8 +1146,6 @@
|
||||
"screen_polls_history_filter_ongoing" = "Текущи";
|
||||
"screen_polls_history_filter_past" = "Приключили";
|
||||
"screen_polls_history_title" = "Анкети";
|
||||
"screen_preferences_user_status_custom_hint" = "Custom status…";
|
||||
"screen_preferences_user_status_placeholder" = "What's your status?";
|
||||
"screen_push_history_title" = "Push history";
|
||||
"screen_qr_code_login_connecting_subtitle" = "Establishing a secure connection";
|
||||
"screen_qr_code_login_connection_note_secure_state_description" = "A secure connection could not be made to the new device. Your existing devices are still safe and you don't need to worry about them.";
|
||||
@@ -1204,7 +1223,7 @@
|
||||
"screen_report_content_hint" = "Причина за докладване на това съдържание";
|
||||
"screen_report_room_leave_failed_alert_message" = "Your report was submitted successfully, but we encountered an issue while trying to leave the room. Please try again.";
|
||||
"screen_report_room_leave_failed_alert_title" = "Unable to Leave Room";
|
||||
"screen_report_room_reason_footer" = "Report this room to your admin. If the messages are encrypted, your admin will not be able to read them.";
|
||||
"screen_report_room_reason_footer" = "Report this chat to your account provider. If the messages are encrypted, your account provider will not be able to read them.";
|
||||
"screen_report_room_reason_placeholder" = "Describe the reason to report…";
|
||||
"screen_report_room_title" = "Докладване на стаята";
|
||||
"screen_reset_encryption_confirmation_alert_action" = "Yes, reset now";
|
||||
@@ -1271,6 +1290,10 @@
|
||||
"screen_room_change_role_section_users" = "Членове";
|
||||
"screen_room_change_role_unsaved_changes_description" = "You have unsaved changes.";
|
||||
"screen_room_change_role_unsaved_changes_title" = "Save changes?";
|
||||
"screen_room_confirm_removal_message" = "Are you sure you wish to remove this message?";
|
||||
"screen_room_confirm_removal_reason_label" = "Reason (optional)";
|
||||
"screen_room_confirm_removal_reason_placeholder" = "Enter…";
|
||||
"screen_room_confirm_removal_title" = "Confirm removal";
|
||||
"screen_room_details_add_topic_title" = "Добавяне на тема";
|
||||
"screen_room_details_badge_encrypted" = "С шифроване";
|
||||
"screen_room_details_badge_not_encrypted" = "Без шифроване";
|
||||
@@ -1316,6 +1339,7 @@
|
||||
"screen_room_invite_again_alert_message" = "Искате ли да ги поканите обратно?";
|
||||
"screen_room_invite_again_alert_title" = "Вие сте сами в този чат";
|
||||
"screen_room_live_location_banner" = "Sharing live location";
|
||||
"screen_room_maximum_messages_selected" = "Maximum messages selected";
|
||||
"screen_room_member_details_block_alert_action" = "Блокиране";
|
||||
"screen_room_member_details_block_alert_description" = "Блокираните потребители няма да могат да ви изпращат съобщения и всички техни съобщения ще бъдат скрити. Можете да ги отблокирате по всяко време.";
|
||||
"screen_room_member_details_block_user" = "Блокиране на потребителя";
|
||||
@@ -1354,9 +1378,10 @@
|
||||
"screen_room_notification_settings_error_loading_settings" = "Възникна грешка при зареждането на настройките за известия.";
|
||||
"screen_room_notification_settings_error_restoring_default" = "Неуспешно възстановяване на режима по подразбиране, моля, опитайте отново.";
|
||||
"screen_room_notification_settings_error_setting_mode" = "Неуспешно задаване на режима, моля, опитайте отново.";
|
||||
"screen_room_notification_settings_mentions_only_disclaimer" = "Your homeserver does not support this option in encrypted rooms, you won't get notified in this room.";
|
||||
"screen_room_notification_settings_mentions_only_disclaimer" = "Your account provider does not support this option in encrypted chats, you won't get notified in this chat.";
|
||||
"screen_room_notification_settings_mode_all_messages" = "Всички съобщения";
|
||||
"screen_room_notification_settings_mode_mentions_and_keywords" = "Само споменавания и ключови думи";
|
||||
"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and replies only";
|
||||
"screen_room_notification_settings_mode_mute" = "Mute";
|
||||
"screen_room_notification_settings_room_custom_settings_title" = "В тази стая, да бъда известяван за";
|
||||
"screen_room_pinned_banner_indicator" = "%1$@ of %2$@";
|
||||
"screen_room_pinned_banner_indicator_description" = "%1$@ Pinned messages";
|
||||
@@ -1417,6 +1442,8 @@
|
||||
"screen_roomlist_filter_invites_empty_state_title" = "Нямате чакащи покани.";
|
||||
"screen_roomlist_filter_low_priority" = "Нисък приоритет";
|
||||
"screen_roomlist_filter_low_priority_empty_state_title" = "You don’t have any low priority chats yet";
|
||||
"screen_roomlist_filter_mention" = "Mention";
|
||||
"screen_roomlist_filter_mention_empty_state_title" = "You don’t have any unread mentions";
|
||||
"screen_roomlist_filter_mixed_empty_state_subtitle" = "Можете да премахнете избора на филтрите, за да видите другите си чатове";
|
||||
"screen_roomlist_filter_mixed_empty_state_title" = "You don’t have chats for this selection";
|
||||
"screen_roomlist_filter_people" = "Хора";
|
||||
@@ -1503,6 +1530,8 @@
|
||||
"screen_session_verification_request_title" = "Поискано е потвърждение";
|
||||
"screen_session_verification_they_dont_match" = "Те не съвпадат";
|
||||
"screen_session_verification_they_match" = "Те съвпадат";
|
||||
"screen_session_verification_unavailable_subtitle" = "Please use another device or recovery key.";
|
||||
"screen_session_verification_unavailable_title" = "Verification with this device is currently unavailable";
|
||||
"screen_session_verification_use_another_device_subtitle" = "Уверете се, че приложението е отворено на другото устройство, преди да започнете потвърждението оттук.";
|
||||
"screen_session_verification_use_another_device_title" = "Отворете приложението на друго потвърдено устройство";
|
||||
"screen_session_verification_user_initiator_subtitle" = "For extra security, verify this user by comparing a set of emojis on your devices. Do this by using a trusted way to communicate.";
|
||||
@@ -1515,6 +1544,14 @@
|
||||
"screen_session_verification_waiting_subtitle" = "След като бъдете приети, ще можете да продължите потвърждението.";
|
||||
"screen_session_verification_waiting_to_accept_subtitle" = "Приемете заявката, за да започнете процеса на потвърждаване в другата си сесия, за да продължите.";
|
||||
"screen_session_verification_waiting_to_accept_title" = "В очакване на приемане на заявка";
|
||||
"screen_settings_user_status_away" = "Away";
|
||||
"screen_settings_user_status_be_right_back" = "Be right back";
|
||||
"screen_settings_user_status_custom" = "Custom…";
|
||||
"screen_settings_user_status_custom_hint" = "Custom status…";
|
||||
"screen_settings_user_status_focus_time" = "Focus time";
|
||||
"screen_settings_user_status_in_a_meeting" = "In a meeting";
|
||||
"screen_settings_user_status_on_the_road" = "On the road";
|
||||
"screen_settings_user_status_placeholder" = "What's your status?";
|
||||
"screen_share_location_live_location_disclaimer_title" = "Your live location history will be stored in the room and visible to members after the session ends.";
|
||||
"screen_share_location_live_location_duration_picker_title" = "Choose how long to share your live location.";
|
||||
"screen_share_location_live_location_missing_permissions" = "You do not have permissions to share your live location in this room";
|
||||
@@ -1554,7 +1591,7 @@
|
||||
"screen_space_list_parent_space" = "%1$@ пространство";
|
||||
"screen_space_list_title" = "Пространства";
|
||||
"screen_space_menu_action_members" = "Преглед на членовете";
|
||||
"screen_space_remove_rooms_confirmation_content" = "Removing a room will not affect the room access. To change the access go to Room info > Privacy & security.";
|
||||
"screen_space_remove_rooms_confirmation_content" = "Removing a room will not affect the room access. To change the access go to Room info > Security & privacy.";
|
||||
"screen_space_remove_rooms_confirmation_title_ios" = "Remove rooms from %1$@?";
|
||||
"screen_space_settings_leave_space" = "Напускане на пространството";
|
||||
"screen_space_settings_roles_and_permissions" = "Роли и разрешения";
|
||||
@@ -1705,6 +1742,7 @@
|
||||
"troubleshoot_notifications_test_push_loop_back_failure_2" = "Грешка: %1$@";
|
||||
"troubleshoot_notifications_test_push_loop_back_failure_3" = "Error, cannot test push.";
|
||||
"troubleshoot_notifications_test_push_loop_back_failure_4" = "Error, timeout waiting for push.";
|
||||
"troubleshoot_notifications_test_push_loop_back_failure_rate_limit" = "Error: the push gateway is rate limiting this device, so notifications won't be reliable. Please set up a different push gateway.";
|
||||
"troubleshoot_notifications_test_push_loop_back_success" = "Push loop back took %1$d ms.";
|
||||
"troubleshoot_notifications_test_push_loop_back_title" = "Test Push loop back";
|
||||
"troubleshoot_notifications_test_unified_push_description" = "Ensure that UnifiedPush distributors are available.";
|
||||
|
||||
@@ -18,6 +18,22 @@
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>a11y_gallery_more_media</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Gallery, %1$d more media</string>
|
||||
<key>other</key>
|
||||
<string>Gallery, %1$d more medias</string>
|
||||
</dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>a11y_polls_percent_of_total</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
@@ -242,6 +258,22 @@
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>notification_gallery_body</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Shared %1$d attachment</string>
|
||||
<key>other</key>
|
||||
<string>Shared %1$d attachments</string>
|
||||
</dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>notification_invitations</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
@@ -514,6 +546,22 @@
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>screen_room_selection_count</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%1$d message selected</string>
|
||||
<key>other</key>
|
||||
<string>%1$d messages selected</string>
|
||||
</dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>screen_room_timeline_reaction_a11y</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
|
||||
@@ -6,11 +6,17 @@
|
||||
"a11y_collapse_message_text_field" = "Minimise message text field";
|
||||
"a11y_create_poll_votes_allowed_decrease" = "Decrease votes allowed per person";
|
||||
"a11y_create_poll_votes_allowed_increase" = "Increase votes allowed per person";
|
||||
"a11y_custom_emoji" = "Custom emoji";
|
||||
"a11y_delete" = "Elimina";
|
||||
"a11y_duration" = "Duration: %1$@";
|
||||
"a11y_edit_avatar" = "Edit avatar";
|
||||
"a11y_edit_room_address_hint" = "The full address will be %1$@";
|
||||
"a11y_expand_message_text_field" = "Expand message text field";
|
||||
"a11y_gallery_attachment" = "Gallery, attachment %1$d";
|
||||
"a11y_gallery_failed_scan" = "Gallery, failed scan %1$d";
|
||||
"a11y_gallery_image" = "Gallery, image %1$d";
|
||||
"a11y_gallery_scanning" = "Gallery, scanning %1$d";
|
||||
"a11y_gallery_video" = "Gallery, video %1$d";
|
||||
"a11y_hide_password" = "Amaga contrasenya";
|
||||
"a11y_info" = "Info";
|
||||
"a11y_join_call" = "Uneix-te a la trucada";
|
||||
@@ -51,6 +57,7 @@
|
||||
"a11y_start_call" = "Inicia trucada";
|
||||
"a11y_start_video_call" = "Start a video call";
|
||||
"a11y_start_voice_call" = "Start a voice call";
|
||||
"a11y_status" = "Status";
|
||||
"a11y_tombstoned_room" = "Tombstoned room";
|
||||
"a11y_user_menu" = "Menú d'usuari";
|
||||
"a11y_video_preview" = "Video preview";
|
||||
@@ -167,6 +174,7 @@
|
||||
"action_retry_decryption" = "Torna a intentar desxifrar";
|
||||
"action_save" = "Desa";
|
||||
"action_search" = "Cerca";
|
||||
"action_select" = "Select";
|
||||
"action_select_all" = "Select all";
|
||||
"action_send" = "Envia";
|
||||
"action_send_edited_message" = "Send edited message";
|
||||
@@ -387,13 +395,6 @@
|
||||
"common_unsupported_call" = "Trucada no compatible";
|
||||
"common_unsupported_event" = "Esdeveniment no compatible";
|
||||
"common_user_started_a_call" = "%1$@ started a call";
|
||||
"common_user_status_away" = "Away";
|
||||
"common_user_status_be_right_back" = "Be right back";
|
||||
"common_user_status_custom" = "Custom…";
|
||||
"common_user_status_focus_time" = "Focus time";
|
||||
"common_user_status_in_a_meeting" = "In a meeting";
|
||||
"common_user_status_on_a_call" = "On a call";
|
||||
"common_user_status_on_the_road" = "On the road";
|
||||
"common_username" = "Nom d'usuari";
|
||||
"common_verification_cancelled" = "Verificació cancel·lada";
|
||||
"common_verification_complete" = "Verificació completada";
|
||||
@@ -420,7 +421,7 @@
|
||||
"confirm_recovery_key_banner_secondary_button_title" = "Has oblidat la clau de recuperació?";
|
||||
"confirm_recovery_key_banner_title" = "L'emmagatzematge de claus no està sincronitzat";
|
||||
"content_scanner_not_found" = "Download not available.";
|
||||
"content_scanner_not_found_title" = "File not found";
|
||||
"content_scanner_not_found_title" = "Something went wrong";
|
||||
"content_scanner_scanning" = "Scanning…";
|
||||
"content_scanner_unsafe_message" = "Preview and download have been disabled.";
|
||||
"content_scanner_unsafe_title" = "The file is not safe";
|
||||
@@ -495,6 +496,11 @@
|
||||
"error_no_compatible_app_found" = "No s'ha trobat cap aplicació compatible per gestionar aquesta acció.";
|
||||
"error_room_address_already_exists" = "Aquesta adreça de sala ja existeix. Prova d'editar el camp d'adreça o de canviar el nom de sala";
|
||||
"error_room_address_invalid_symbols" = "Alguns caràcters no estan permesos. Només s'admeten lletres, dígits i els símbols següents: ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _";
|
||||
"error_sending_failure_invalid_mime_type" = "Invalid mime type: %@";
|
||||
"error_sending_failure_missing_media_content" = "Missing media content";
|
||||
"error_sending_failure_other" = "Error details: %@";
|
||||
"error_sending_failure_sending_from_unverified_device" = "Sending from unverified device";
|
||||
"error_sending_failure_unknown" = "Unknown error";
|
||||
"error_some_messages_have_not_been_sent" = "Alguns missatges no s'han enviat";
|
||||
"error_unknown" = "S'ha produït un error";
|
||||
"error.account_already_logged_in" = "You're already logged in on this device as %1$@.";
|
||||
@@ -524,6 +530,8 @@
|
||||
"notification_invitation_action_reject" = "Rebutja";
|
||||
"notification_invite_body" = "T'ha convidat a xatejar";
|
||||
"notification_invite_body_with_sender" = "%1$@ t'ha convidat a xatejar";
|
||||
"notification_knock_request_body" = "Requested to join";
|
||||
"notification_live_location_started_body" = "Started sharing their live location";
|
||||
"notification_mentioned_you_body" = "T'ha mencionat: %1$@";
|
||||
"notification_new_messages" = "Missatges nous";
|
||||
"notification_reaction_body" = "Ha reaccionat amb %1$@";
|
||||
@@ -708,14 +716,22 @@
|
||||
"screen_change_server_error_element_pro_required_title" = "Element Pro required";
|
||||
"screen_change_server_error_invalid_homeserver" = "No s'ha pogut accedir a aquest servidor. Comprova que hagis introduït correctament l'URL del servidor. Si ja és correcte, posa't en contacte amb l'administrador del servidor per a més informació.";
|
||||
"screen_change_server_error_invalid_well_known" = "Servidor no disponible a causa d'un problema al fitxer .well-known:\n%1$@";
|
||||
"screen_change_server_error_no_sliding_sync_message" = "The selected account provider does not support sliding sync. An upgrade to the server is needed to use %1$@.";
|
||||
"screen_change_server_error_no_sliding_sync_message" = "The selected account provider does not support sliding sync. The server needs to be upgraded to use %1$@.";
|
||||
"screen_change_server_error_unauthorized_homeserver" = "%1$@ is not allowed to connect to %2$@.";
|
||||
"screen_change_server_error_unauthorized_homeserver_content" = "This app has been configured to allow: %1$@.";
|
||||
"screen_change_server_error_unauthorized_homeserver_title" = "Account provider %1$@ not allowed.";
|
||||
"screen_change_server_form_header" = "URL del servidor";
|
||||
"screen_change_server_form_notice" = "Introdueix un domini.";
|
||||
"screen_change_server_navigation_title_login" = "Sign in";
|
||||
"screen_change_server_navigation_title_register" = "Create account";
|
||||
"screen_change_server_subtitle" = "Quina és l'adreça del teu servidor?";
|
||||
"screen_change_server_textfield_footer_login" = "If your account is provided by your company or a community, then change the account provider (example: companyname.com).";
|
||||
"screen_change_server_textfield_footer_register" = "Create an account on matrix.org for free. If you want to create an account with a different provider, then change it accordingly (example: communityname.org).";
|
||||
"screen_change_server_textfield_header" = "Account provider";
|
||||
"screen_change_server_textfield_placeholder" = "example.com";
|
||||
"screen_change_server_title" = "Selecciona el teu servidor";
|
||||
"screen_change_server_title_login" = "Enter your account provider";
|
||||
"screen_change_server_title_register" = "Choose an account provider";
|
||||
"screen_chat_backup_key_backup_action_disable" = "Elimina l'emmagatzematge de claus";
|
||||
"screen_chat_backup_key_backup_action_enable" = "Activa còpia de seguretat";
|
||||
"screen_chat_backup_key_backup_description" = "Emmagatzema la teva identitat criptogràfica i les claus de missatge de forma segura al servidor. Això et permetrà veure l'historial de missatges en qualsevol dispositiu nou. %1$@.";
|
||||
@@ -762,7 +778,7 @@
|
||||
"screen_create_room_private_option_description" = "Només s'hi poden unir les persones convidades.";
|
||||
"screen_create_room_private_option_title" = "Private";
|
||||
"screen_create_room_public_option_description" = "Tothom pot trobar aquesta sala.\nPots canviar-ho en qualsevol moment a la configuració de sala.";
|
||||
"screen_create_room_public_option_short_description" = "Anyone can join.";
|
||||
"screen_create_room_public_option_short_description" = "Anyone can join, including people outside your organisation.";
|
||||
"screen_create_room_public_option_title" = "Public";
|
||||
"screen_create_room_room_access_section_knocking_option_description" = "Qualsevol persona pot sol·licitar unir-s'hi però un administrador o moderador l'haurà d'acceptar";
|
||||
"screen_create_room_room_access_section_knocking_option_title" = "Permet sol·licituds d'unió";
|
||||
@@ -935,6 +951,10 @@
|
||||
"screen_knock_requests_list_empty_state_title" = "No hi ha sol·licituds d'unió pendents";
|
||||
"screen_knock_requests_list_initial_loading_title" = "Carregant sol·licituds d'unió…";
|
||||
"screen_knock_requests_list_title" = "Sol·licituds d'unió";
|
||||
"screen_labs_enable_gallery" = "Gallery messages";
|
||||
"screen_labs_enable_gallery_description" = "Send multiple images or videos in one go. Stable on mobile, but not yet supported by web.";
|
||||
"screen_labs_enable_knocking" = "Ask to join";
|
||||
"screen_labs_enable_knocking_description" = "Users can request access to a room that is not open for anyone to join. Stable on mobile, web implementation exists but is experimental.";
|
||||
"screen_labs_enable_threads" = "Enable thread replies";
|
||||
"screen_labs_enable_threads_description" = "The app will restart to apply this change.";
|
||||
"screen_labs_header_description" = "Try out our latest ideas in development. These features are not finalised; they may be unstable, may change.";
|
||||
@@ -943,11 +963,11 @@
|
||||
"screen_leave_space_choose_owners_action" = "Choose owners";
|
||||
"screen_leave_space_last_admin_info" = "%1$@ (Admin)";
|
||||
"screen_leave_space_subtitle" = "Select the rooms you’d like to leave which you're not the only administrator for:";
|
||||
"screen_leave_space_subtitle_last_admin" = "You need to assign another admin for this space before you can leave.";
|
||||
"screen_leave_space_subtitle_last_admin" = "You need to assign another administrator for this space before you can leave.";
|
||||
"screen_leave_space_subtitle_last_owner" = "You are the only owner of %1$@. You need to transfer ownership to someone else before you leave.";
|
||||
"screen_leave_space_subtitle_only_last_admin" = "You will not be removed from the following room(s) because you're the only administrator:";
|
||||
"screen_leave_space_title" = "Leave %1$@?";
|
||||
"screen_leave_space_title_last_admin" = "You are the only admin for %1$@";
|
||||
"screen_leave_space_title_last_admin" = "You are the only administrator for %1$@";
|
||||
"screen_leave_space_title_last_owner" = "Transfer ownership";
|
||||
"screen_link_new_device_authentication_reason_ios" = "Confirm it's you before linking a new device.";
|
||||
"screen_link_new_device_desktop_scanning_title" = "Escaneja el QR";
|
||||
@@ -955,6 +975,7 @@
|
||||
"screen_link_new_device_desktop_step3" = "Escaneja el codi QR amb aquest dispositiu";
|
||||
"screen_link_new_device_desktop_submit" = "Preparat per escanejar";
|
||||
"screen_link_new_device_desktop_title" = "Open %1$@ on a desktop computer to get the QR code";
|
||||
"screen_link_new_device_desktop_warning" = "Only scan a QR code after following the steps above on your own device. Never scan a code sent by another person.";
|
||||
"screen_link_new_device_enter_number_error_numbers_do_not_match" = "The numbers don’t match";
|
||||
"screen_link_new_device_enter_number_notice" = "Enter 2-digit code";
|
||||
"screen_link_new_device_enter_number_subtitle" = "This will verify that the connection to your other device is secure.";
|
||||
@@ -981,7 +1002,7 @@
|
||||
"screen_live_location_sheet_nobody_sharing" = "Nobody is sharing their location";
|
||||
"screen_live_location_sheet_sharing_live_location" = "Sharing live location";
|
||||
"screen_live_location_sheet_title" = "On the map";
|
||||
"screen_local_network_opt_in_subtitle" = "Your homeserver is on your local network. To connect, Element needs permission to reach devices on this network.";
|
||||
"screen_local_network_opt_in_subtitle" = "Your homeserver is on your local network. To connect, %1$@ needs permission to reach devices on this network.";
|
||||
"screen_local_network_opt_in_title" = "Allow access to local network";
|
||||
"screen_login_error_deactivated_account" = "Aquest compte s'ha desactivat.";
|
||||
"screen_login_error_invalid_credentials" = "Usuari i/o contrasenya incorrectes";
|
||||
@@ -1033,7 +1054,7 @@
|
||||
"screen_missing_key_backup_open_element_classic" = "Open Element Classic";
|
||||
"screen_missing_key_backup_step_1" = "Open Element Classic on your device";
|
||||
"screen_missing_key_backup_step_2_ios" = "Go to User settings > Security";
|
||||
"screen_missing_key_backup_step_3_ios" = "Scroll down to Secure Backup and tap on Set up";
|
||||
"screen_missing_key_backup_step_3_ios" = "Scroll down to Chat Backup and tap on Set up";
|
||||
"screen_missing_key_backup_step_4" = "Follow the instructions to enable your key storage";
|
||||
"screen_missing_key_backup_step_5" = "Come back to %1$@";
|
||||
"screen_missing_key_backup_title" = "Enable your key storage before proceeding to %1$@";
|
||||
@@ -1125,8 +1146,6 @@
|
||||
"screen_polls_history_filter_ongoing" = "En curs";
|
||||
"screen_polls_history_filter_past" = "Pasades";
|
||||
"screen_polls_history_title" = "Votacions";
|
||||
"screen_preferences_user_status_custom_hint" = "Custom status…";
|
||||
"screen_preferences_user_status_placeholder" = "What's your status?";
|
||||
"screen_push_history_title" = "Push history";
|
||||
"screen_qr_code_login_connecting_subtitle" = "Establint una connexió segura";
|
||||
"screen_qr_code_login_connection_note_secure_state_description" = "No s'ha pogut establir una connexió segura amb el dispositiu nou. Els dispositius existents continuen sent segurs, no te n'has de preocupar.";
|
||||
@@ -1271,6 +1290,10 @@
|
||||
"screen_room_change_role_section_users" = "Membres";
|
||||
"screen_room_change_role_unsaved_changes_description" = "Hi ha canvis sense desar.";
|
||||
"screen_room_change_role_unsaved_changes_title" = "Desar canvis?";
|
||||
"screen_room_confirm_removal_message" = "Are you sure you wish to remove this message?";
|
||||
"screen_room_confirm_removal_reason_label" = "Reason (optional)";
|
||||
"screen_room_confirm_removal_reason_placeholder" = "Enter…";
|
||||
"screen_room_confirm_removal_title" = "Confirm removal";
|
||||
"screen_room_details_add_topic_title" = "Afegeix tema";
|
||||
"screen_room_details_badge_encrypted" = "Xifrada";
|
||||
"screen_room_details_badge_not_encrypted" = "No xifrada";
|
||||
@@ -1316,6 +1339,7 @@
|
||||
"screen_room_invite_again_alert_message" = "Vols tornar-los a convidar-los?";
|
||||
"screen_room_invite_again_alert_title" = "No hi ha ningú més al xat";
|
||||
"screen_room_live_location_banner" = "Sharing live location";
|
||||
"screen_room_maximum_messages_selected" = "Maximum messages selected";
|
||||
"screen_room_member_details_block_alert_action" = "Bloqueja";
|
||||
"screen_room_member_details_block_alert_description" = "Els usuaris bloquejats no podran enviar-te missatges i tots els seus missatges s'amagaran. Pots desbloquejar-los en qualsevol moment.";
|
||||
"screen_room_member_details_block_user" = "Bloqueja usuari";
|
||||
@@ -1355,8 +1379,9 @@
|
||||
"screen_room_notification_settings_error_restoring_default" = "No s'ha pogut restaurar el mode predeterminat. Torna-ho a provar.";
|
||||
"screen_room_notification_settings_error_setting_mode" = "No s'ha pogut configurar el mode. Torna-ho a provar.";
|
||||
"screen_room_notification_settings_mentions_only_disclaimer" = "El servidor no admet aquesta opció en sales xifrades, no rebras notificacions en aquesta sala.";
|
||||
"screen_room_notification_settings_mode_all_messages" = "Tots els missatges";
|
||||
"screen_room_notification_settings_mode_mentions_and_keywords" = "Mencions y paraules clau (només)";
|
||||
"screen_room_notification_settings_mode_all_messages" = "All messages";
|
||||
"screen_room_notification_settings_mode_mentions_and_keywords" = "Mentions and replies only";
|
||||
"screen_room_notification_settings_mode_mute" = "Mute";
|
||||
"screen_room_notification_settings_room_custom_settings_title" = "En aquesta sala, notifica'm en";
|
||||
"screen_room_pinned_banner_indicator" = "%1$@ de %2$@";
|
||||
"screen_room_pinned_banner_indicator_description" = "%1$@ missatges fixats";
|
||||
@@ -1417,6 +1442,8 @@
|
||||
"screen_roomlist_filter_invites_empty_state_title" = "No tens cap invitació pendent.";
|
||||
"screen_roomlist_filter_low_priority" = "Prioritat baixa";
|
||||
"screen_roomlist_filter_low_priority_empty_state_title" = "You don’t have any low priority chats yet";
|
||||
"screen_roomlist_filter_mention" = "Mention";
|
||||
"screen_roomlist_filter_mention_empty_state_title" = "You don’t have any unread mentions";
|
||||
"screen_roomlist_filter_mixed_empty_state_subtitle" = "Pots desseleccionar els filtres per veure els altres xats";
|
||||
"screen_roomlist_filter_mixed_empty_state_title" = "Cap xats per a aquesta selecció";
|
||||
"screen_roomlist_filter_people" = "Persones";
|
||||
@@ -1503,6 +1530,8 @@
|
||||
"screen_session_verification_request_title" = "Verificació sol·licitada";
|
||||
"screen_session_verification_they_dont_match" = "No coincideixen";
|
||||
"screen_session_verification_they_match" = "Coincideixen";
|
||||
"screen_session_verification_unavailable_subtitle" = "Please use another device or recovery key.";
|
||||
"screen_session_verification_unavailable_title" = "Verification with this device is currently unavailable";
|
||||
"screen_session_verification_use_another_device_subtitle" = "Assegura't que tens l'aplicació oberta a l'altre dispositiu abans d'inicar la verificació des d'aquí.";
|
||||
"screen_session_verification_use_another_device_title" = "Obre l'aplicació en un altre dispositiu verificat";
|
||||
"screen_session_verification_user_initiator_subtitle" = "Per a més seguretat, verifica aquest usuari comparant un conjunt d'emoticones vostres dispositius. Fes-ho a través d'un mitjà de comunicació fiable, o en persona.";
|
||||
@@ -1515,6 +1544,14 @@
|
||||
"screen_session_verification_waiting_subtitle" = "Un cop acceptat, podràs continuar amb la verificació.";
|
||||
"screen_session_verification_waiting_to_accept_subtitle" = "Per continuar, accepta la sol·licitud per iniciar el procés de verificació a l'altra sessió.";
|
||||
"screen_session_verification_waiting_to_accept_title" = "Esperant que s'accepti la sol·licitud";
|
||||
"screen_settings_user_status_away" = "Away";
|
||||
"screen_settings_user_status_be_right_back" = "Be right back";
|
||||
"screen_settings_user_status_custom" = "Custom…";
|
||||
"screen_settings_user_status_custom_hint" = "Custom status…";
|
||||
"screen_settings_user_status_focus_time" = "Focus time";
|
||||
"screen_settings_user_status_in_a_meeting" = "In a meeting";
|
||||
"screen_settings_user_status_on_the_road" = "On the road";
|
||||
"screen_settings_user_status_placeholder" = "What's your status?";
|
||||
"screen_share_location_live_location_disclaimer_title" = "Your live location history will be stored in the room and visible to members after the session ends.";
|
||||
"screen_share_location_live_location_duration_picker_title" = "Choose how long to share your live location.";
|
||||
"screen_share_location_live_location_missing_permissions" = "You do not have permissions to share your live location in this room";
|
||||
@@ -1554,7 +1591,7 @@
|
||||
"screen_space_list_parent_space" = "%1$@ space";
|
||||
"screen_space_list_title" = "Spaces";
|
||||
"screen_space_menu_action_members" = "View members";
|
||||
"screen_space_remove_rooms_confirmation_content" = "Removing a room will not affect the room access. To change the access go to Room info > Privacy & security.";
|
||||
"screen_space_remove_rooms_confirmation_content" = "Removing a room will not affect the room access. To change the access go to Room info > Security & privacy.";
|
||||
"screen_space_remove_rooms_confirmation_title_ios" = "Remove rooms from %1$@?";
|
||||
"screen_space_settings_leave_space" = "Leave space";
|
||||
"screen_space_settings_roles_and_permissions" = "Rols i permisos";
|
||||
@@ -1705,6 +1742,7 @@
|
||||
"troubleshoot_notifications_test_push_loop_back_failure_2" = "Error: %1$@.";
|
||||
"troubleshoot_notifications_test_push_loop_back_failure_3" = "Error, no s'han pogut provar les notificacions push.";
|
||||
"troubleshoot_notifications_test_push_loop_back_failure_4" = "Error, s'ha acabat el temps d'espera de la notificació push.";
|
||||
"troubleshoot_notifications_test_push_loop_back_failure_rate_limit" = "Error: the push gateway is rate limiting this device, so notifications won't be reliable. Please set up a different push gateway.";
|
||||
"troubleshoot_notifications_test_push_loop_back_success" = "El bucle de notificacions push ha tardat %1$d ms.";
|
||||
"troubleshoot_notifications_test_push_loop_back_title" = "Prova el bucle de notificacions push";
|
||||
"troubleshoot_notifications_test_unified_push_description" = "Assegura't que hi hagi distribuïdors d'UnifiedPush disponibles.";
|
||||
|
||||
@@ -18,6 +18,22 @@
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>a11y_gallery_more_media</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Gallery, %1$d more media</string>
|
||||
<key>other</key>
|
||||
<string>Gallery, %1$d more medias</string>
|
||||
</dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>a11y_polls_percent_of_total</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
@@ -242,6 +258,22 @@
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>notification_gallery_body</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Shared %1$d attachment</string>
|
||||
<key>other</key>
|
||||
<string>Shared %1$d attachments</string>
|
||||
</dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>notification_invitations</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
@@ -514,6 +546,22 @@
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>screen_room_selection_count</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%1$d message selected</string>
|
||||
<key>other</key>
|
||||
<string>%1$d messages selected</string>
|
||||
</dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>screen_room_timeline_reaction_a11y</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
|
||||
@@ -6,11 +6,17 @@
|
||||
"a11y_collapse_message_text_field" = "Minimalizovat textové pole zprávy";
|
||||
"a11y_create_poll_votes_allowed_decrease" = "Decrease votes allowed per person";
|
||||
"a11y_create_poll_votes_allowed_increase" = "Increase votes allowed per person";
|
||||
"a11y_custom_emoji" = "Custom emoji";
|
||||
"a11y_delete" = "Smazat";
|
||||
"a11y_duration" = "Duration: %1$@";
|
||||
"a11y_edit_avatar" = "Upravit avatar";
|
||||
"a11y_edit_room_address_hint" = "Úplná adresa bude %1$@";
|
||||
"a11y_expand_message_text_field" = "Rozbalit textové pole zprávy";
|
||||
"a11y_gallery_attachment" = "Gallery, attachment %1$d";
|
||||
"a11y_gallery_failed_scan" = "Gallery, failed scan %1$d";
|
||||
"a11y_gallery_image" = "Gallery, image %1$d";
|
||||
"a11y_gallery_scanning" = "Gallery, scanning %1$d";
|
||||
"a11y_gallery_video" = "Gallery, video %1$d";
|
||||
"a11y_hide_password" = "Skrýt heslo";
|
||||
"a11y_info" = "Informace";
|
||||
"a11y_join_call" = "Připojit se k hovoru";
|
||||
@@ -51,6 +57,7 @@
|
||||
"a11y_start_call" = "Zahájit hovor";
|
||||
"a11y_start_video_call" = "Zahájit videohovor";
|
||||
"a11y_start_voice_call" = "Zahájit hlasový hovor";
|
||||
"a11y_status" = "Status";
|
||||
"a11y_tombstoned_room" = "Místnost s náhrobkem";
|
||||
"a11y_user_menu" = "Uživatelské menu";
|
||||
"a11y_video_preview" = "Video preview";
|
||||
@@ -167,6 +174,7 @@
|
||||
"action_retry_decryption" = "Opakovat dešifrování";
|
||||
"action_save" = "Uložit";
|
||||
"action_search" = "Hledat";
|
||||
"action_select" = "Select";
|
||||
"action_select_all" = "Vybrat vše";
|
||||
"action_send" = "Odeslat";
|
||||
"action_send_edited_message" = "Odeslat upravenou zprávu";
|
||||
@@ -387,13 +395,6 @@
|
||||
"common_unsupported_call" = "Nepodporované volání";
|
||||
"common_unsupported_event" = "Nepodporovaná událost";
|
||||
"common_user_started_a_call" = "%1$@ started a call";
|
||||
"common_user_status_away" = "Away";
|
||||
"common_user_status_be_right_back" = "Be right back";
|
||||
"common_user_status_custom" = "Custom…";
|
||||
"common_user_status_focus_time" = "Focus time";
|
||||
"common_user_status_in_a_meeting" = "In a meeting";
|
||||
"common_user_status_on_a_call" = "On a call";
|
||||
"common_user_status_on_the_road" = "On the road";
|
||||
"common_username" = "Uživatelské jméno";
|
||||
"common_verification_cancelled" = "Ověření zrušeno";
|
||||
"common_verification_complete" = "Ověření dokončeno";
|
||||
@@ -420,7 +421,7 @@
|
||||
"confirm_recovery_key_banner_secondary_button_title" = "Zapomněli jste klíč pro obnovení?";
|
||||
"confirm_recovery_key_banner_title" = "Vaše úložiště klíčů není synchronizováno";
|
||||
"content_scanner_not_found" = "Download not available.";
|
||||
"content_scanner_not_found_title" = "File not found";
|
||||
"content_scanner_not_found_title" = "Something went wrong";
|
||||
"content_scanner_scanning" = "Scanning…";
|
||||
"content_scanner_unsafe_message" = "Preview and download have been disabled.";
|
||||
"content_scanner_unsafe_title" = "The file is not safe";
|
||||
@@ -495,6 +496,11 @@
|
||||
"error_no_compatible_app_found" = "Nebyla nalezena žádná kompatibilní aplikace, která by tuto akci zpracovala.";
|
||||
"error_room_address_already_exists" = "Tato adresa místnosti již existuje, zkuste prosím upravit pole adresy místnosti nebo změnit název místnosti";
|
||||
"error_room_address_invalid_symbols" = "Některé znaky nejsou povoleny. Podporovány jsou pouze písmena, číslice a následující symboly ! $ & ‘ ( ) * + / ; = ? @ [ ] - . _";
|
||||
"error_sending_failure_invalid_mime_type" = "Invalid mime type: %@";
|
||||
"error_sending_failure_missing_media_content" = "Missing media content";
|
||||
"error_sending_failure_other" = "Error details: %@";
|
||||
"error_sending_failure_sending_from_unverified_device" = "Sending from unverified device";
|
||||
"error_sending_failure_unknown" = "Unknown error";
|
||||
"error_some_messages_have_not_been_sent" = "Některé zprávy nebyly odeslány";
|
||||
"error_unknown" = "Omlouváme se, došlo k chybě";
|
||||
"error.account_already_logged_in" = "Na tomto zařízení jste již přihlášeni jako %1$@.";
|
||||
@@ -524,6 +530,8 @@
|
||||
"notification_invitation_action_reject" = "Odmítnout";
|
||||
"notification_invite_body" = "Vás pozval(a) do chatu";
|
||||
"notification_invite_body_with_sender" = "%1$@ vás pozval(a) do chatu";
|
||||
"notification_knock_request_body" = "Requested to join";
|
||||
"notification_live_location_started_body" = "Started sharing their live location";
|
||||
"notification_mentioned_you_body" = "Zmínili vás: %1$@";
|
||||
"notification_new_messages" = "Nové zprávy";
|
||||
"notification_reaction_body" = "Reagoval(a) s %1$@";
|
||||
@@ -714,8 +722,16 @@
|
||||
"screen_change_server_error_unauthorized_homeserver_title" = "Poskytovatel účtu %1$@ není povolen.";
|
||||
"screen_change_server_form_header" = "Adresa URL domovského serveru";
|
||||
"screen_change_server_form_notice" = "Zadejte adresu domény.";
|
||||
"screen_change_server_navigation_title_login" = "Sign in";
|
||||
"screen_change_server_navigation_title_register" = "Create account";
|
||||
"screen_change_server_subtitle" = "Jaká je adresa vašeho serveru?";
|
||||
"screen_change_server_textfield_footer_login" = "If your account is provided by your company or a community, then change the account provider (example: companyname.com).";
|
||||
"screen_change_server_textfield_footer_register" = "Create an account on matrix.org for free. If you want to create an account with a different provider, then change it accordingly (example: communityname.org).";
|
||||
"screen_change_server_textfield_header" = "Account provider";
|
||||
"screen_change_server_textfield_placeholder" = "example.com";
|
||||
"screen_change_server_title" = "Vyberte váš server";
|
||||
"screen_change_server_title_login" = "Enter your account provider";
|
||||
"screen_change_server_title_register" = "Choose an account provider";
|
||||
"screen_chat_backup_key_backup_action_disable" = "Vypnout zálohování";
|
||||
"screen_chat_backup_key_backup_action_enable" = "Zapnout zálohování";
|
||||
"screen_chat_backup_key_backup_description" = "To vám umožní zobrazit historii chatu na všech nových zařízeních a je to nutné pro zálohování chatů a digitální identity. %1$@ .";
|
||||
@@ -935,6 +951,10 @@
|
||||
"screen_knock_requests_list_empty_state_title" = "Žádná čekající žádost o vstup";
|
||||
"screen_knock_requests_list_initial_loading_title" = "Načítání žádostí o vstup...";
|
||||
"screen_knock_requests_list_title" = "Žádosti o vstup";
|
||||
"screen_labs_enable_gallery" = "Gallery messages";
|
||||
"screen_labs_enable_gallery_description" = "Send multiple images or videos in one go. Stable on mobile, but not yet supported by web.";
|
||||
"screen_labs_enable_knocking" = "Ask to join";
|
||||
"screen_labs_enable_knocking_description" = "Users can request access to a room that is not open for anyone to join. Stable on mobile, web implementation exists but is experimental.";
|
||||
"screen_labs_enable_threads" = "Povolit odpovědi ve vlákně";
|
||||
"screen_labs_enable_threads_description" = "Aplikace se restartuje, aby se tato změna projevila.";
|
||||
"screen_labs_header_description" = "Vyzkoušejte naše nejnovější nápady, které jsou ve vývoji. Tyto funkce nejsou finalizované; mohou být nestabilní a mohou se změnit.";
|
||||
@@ -955,6 +975,7 @@
|
||||
"screen_link_new_device_desktop_step3" = "Naskenujte QR kód pomocí tohoto zařízení";
|
||||
"screen_link_new_device_desktop_submit" = "Připraveno ke skenování";
|
||||
"screen_link_new_device_desktop_title" = "Otevřete %1$@ na stolním počítači a získejte QR kód";
|
||||
"screen_link_new_device_desktop_warning" = "Only scan a QR code after following the steps above on your own device. Never scan a code sent by another person.";
|
||||
"screen_link_new_device_enter_number_error_numbers_do_not_match" = "Čísla se neshodují";
|
||||
"screen_link_new_device_enter_number_notice" = "Zadejte dvoumístný kód";
|
||||
"screen_link_new_device_enter_number_subtitle" = "Tím ověříte, zda je připojení k druhému zařízení bezpečné.";
|
||||
@@ -981,7 +1002,7 @@
|
||||
"screen_live_location_sheet_nobody_sharing" = "Nikdo nesdílí svou polohu";
|
||||
"screen_live_location_sheet_sharing_live_location" = "Sdílení aktuální polohy";
|
||||
"screen_live_location_sheet_title" = "Na mapě";
|
||||
"screen_local_network_opt_in_subtitle" = "Your homeserver is on your local network. To connect, Element needs permission to reach devices on this network.";
|
||||
"screen_local_network_opt_in_subtitle" = "Your homeserver is on your local network. To connect, %1$@ needs permission to reach devices on this network.";
|
||||
"screen_local_network_opt_in_title" = "Allow access to local network";
|
||||
"screen_login_error_deactivated_account" = "Tento účet byl smazán.";
|
||||
"screen_login_error_invalid_credentials" = "Nesprávné uživatelské jméno nebo heslo";
|
||||
@@ -1125,8 +1146,6 @@
|
||||
"screen_polls_history_filter_ongoing" = "Probíhající";
|
||||
"screen_polls_history_filter_past" = "Minulé";
|
||||
"screen_polls_history_title" = "Hlasování";
|
||||
"screen_preferences_user_status_custom_hint" = "Custom status…";
|
||||
"screen_preferences_user_status_placeholder" = "What's your status?";
|
||||
"screen_push_history_title" = "Historie push oznámení";
|
||||
"screen_qr_code_login_connecting_subtitle" = "Navazování zabezpečeného spojení";
|
||||
"screen_qr_code_login_connection_note_secure_state_description" = "K novému zařízení se nepodařilo navázat bezpečné připojení. Vaše stávající zařízení jsou stále v bezpečí a nemusíte se o ně obávat.";
|
||||
@@ -1271,6 +1290,10 @@
|
||||
"screen_room_change_role_section_users" = "Členové";
|
||||
"screen_room_change_role_unsaved_changes_description" = "Máte neuložené změny.";
|
||||
"screen_room_change_role_unsaved_changes_title" = "Uložit změny?";
|
||||
"screen_room_confirm_removal_message" = "Are you sure you wish to remove this message?";
|
||||
"screen_room_confirm_removal_reason_label" = "Reason (optional)";
|
||||
"screen_room_confirm_removal_reason_placeholder" = "Enter…";
|
||||
"screen_room_confirm_removal_title" = "Confirm removal";
|
||||
"screen_room_details_add_topic_title" = "Přidat téma";
|
||||
"screen_room_details_badge_encrypted" = "Šifrováno";
|
||||
"screen_room_details_badge_not_encrypted" = "Není šifrováno";
|
||||
@@ -1316,6 +1339,7 @@
|
||||
"screen_room_invite_again_alert_message" = "Chtěli byste je pozvat zpět?";
|
||||
"screen_room_invite_again_alert_title" = "V tomto chatu jste sami";
|
||||
"screen_room_live_location_banner" = "Sdílení aktuální polohy";
|
||||
"screen_room_maximum_messages_selected" = "Maximum messages selected";
|
||||
"screen_room_member_details_block_alert_action" = "Zablokovat";
|
||||
"screen_room_member_details_block_alert_description" = "Blokovaní uživatelé vám nebudou moci posílat zprávy a všechny jejich zprávy budou skryty. Můžete je kdykoli odblokovat.";
|
||||
"screen_room_member_details_block_user" = "Zablokovat uživatele";
|
||||
@@ -1357,6 +1381,7 @@
|
||||
"screen_room_notification_settings_mentions_only_disclaimer" = "Váš domovský server tuto možnost nepodporuje v šifrovaných místnostech, v této místnosti nebudete dostávat upozornění.";
|
||||
"screen_room_notification_settings_mode_all_messages" = "Všechny zprávy";
|
||||
"screen_room_notification_settings_mode_mentions_and_keywords" = "Pouze zmínky a klíčová slova";
|
||||
"screen_room_notification_settings_mode_mute" = "Mute";
|
||||
"screen_room_notification_settings_room_custom_settings_title" = "V této místnosti mě upozornit na";
|
||||
"screen_room_pinned_banner_indicator" = "%1$@ z %2$@";
|
||||
"screen_room_pinned_banner_indicator_description" = "%1$@ Připnuté zprávy";
|
||||
@@ -1417,6 +1442,8 @@
|
||||
"screen_roomlist_filter_invites_empty_state_title" = "Nemáte žádné nevyřízené pozvánky.";
|
||||
"screen_roomlist_filter_low_priority" = "Nízká priorita";
|
||||
"screen_roomlist_filter_low_priority_empty_state_title" = "Zatím nemáte žádné chaty s nízkou prioritou";
|
||||
"screen_roomlist_filter_mention" = "Mention";
|
||||
"screen_roomlist_filter_mention_empty_state_title" = "You don’t have any unread mentions";
|
||||
"screen_roomlist_filter_mixed_empty_state_subtitle" = "Můžete zrušit výběr filtrů, abyste viděli své další chaty";
|
||||
"screen_roomlist_filter_mixed_empty_state_title" = "Nemáte chaty pro tento výběr";
|
||||
"screen_roomlist_filter_people" = "Lidé";
|
||||
@@ -1503,6 +1530,8 @@
|
||||
"screen_session_verification_request_title" = "Požadováno ověření";
|
||||
"screen_session_verification_they_dont_match" = "Neshodují se";
|
||||
"screen_session_verification_they_match" = "Shodují se";
|
||||
"screen_session_verification_unavailable_subtitle" = "Please use another device or recovery key.";
|
||||
"screen_session_verification_unavailable_title" = "Verification with this device is currently unavailable";
|
||||
"screen_session_verification_use_another_device_subtitle" = "Před zahájením ověřování se ujistěte, že máte aplikaci otevřenou na druhém zařízení.";
|
||||
"screen_session_verification_use_another_device_title" = "Otevřete aplikaci na jiném ověřeném zařízení";
|
||||
"screen_session_verification_user_initiator_subtitle" = "Pro větší bezpečnost ověřte tohoto uživatele porovnáním sady emotikonů na svých zařízeních. Proveďte to pomocí důvěryhodného způsobu komunikace.";
|
||||
@@ -1515,6 +1544,14 @@
|
||||
"screen_session_verification_waiting_subtitle" = "Po přijetí budete moci pokračovat v ověřování.";
|
||||
"screen_session_verification_waiting_to_accept_subtitle" = "Pro pokračování přijměte požadavek na zahájení ověření v jiné relaci.";
|
||||
"screen_session_verification_waiting_to_accept_title" = "Čekání na přijetí žádosti";
|
||||
"screen_settings_user_status_away" = "Away";
|
||||
"screen_settings_user_status_be_right_back" = "Be right back";
|
||||
"screen_settings_user_status_custom" = "Custom…";
|
||||
"screen_settings_user_status_custom_hint" = "Custom status…";
|
||||
"screen_settings_user_status_focus_time" = "Focus time";
|
||||
"screen_settings_user_status_in_a_meeting" = "In a meeting";
|
||||
"screen_settings_user_status_on_the_road" = "On the road";
|
||||
"screen_settings_user_status_placeholder" = "What's your status?";
|
||||
"screen_share_location_live_location_disclaimer_title" = "Vaše historie aktuální polohy bude uložena v místnosti a bude viditelná pro členy i po skončení relace.";
|
||||
"screen_share_location_live_location_duration_picker_title" = "Zvolte, jak dlouho chcete sdílet svou aktuální polohu.";
|
||||
"screen_share_location_live_location_missing_permissions" = "Nemáte oprávnění sdílet svou aktuální polohu v této místnosti.";
|
||||
@@ -1705,6 +1742,7 @@
|
||||
"troubleshoot_notifications_test_push_loop_back_failure_2" = "Chyba: %1$@.";
|
||||
"troubleshoot_notifications_test_push_loop_back_failure_3" = "Chyba, nelze otestovat push.";
|
||||
"troubleshoot_notifications_test_push_loop_back_failure_4" = "Chyba, časový limit čekání na push.";
|
||||
"troubleshoot_notifications_test_push_loop_back_failure_rate_limit" = "Error: the push gateway is rate limiting this device, so notifications won't be reliable. Please set up a different push gateway.";
|
||||
"troubleshoot_notifications_test_push_loop_back_success" = "Push zpětná smyčka trvala %1$d ms.";
|
||||
"troubleshoot_notifications_test_push_loop_back_title" = "Otestovat push pomocí zpětného volání";
|
||||
"troubleshoot_notifications_test_unified_push_description" = "Ujistěte se, že jsou k dispozici distributoři UnifiedPush.";
|
||||
|
||||
@@ -20,6 +20,22 @@
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>a11y_gallery_more_media</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Gallery, %1$d more media</string>
|
||||
<key>other</key>
|
||||
<string>Gallery, %1$d more medias</string>
|
||||
</dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>a11y_polls_percent_of_total</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
@@ -264,6 +280,22 @@
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>notification_gallery_body</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>Shared %1$d attachment</string>
|
||||
<key>other</key>
|
||||
<string>Shared %1$d attachments</string>
|
||||
</dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>notification_invitations</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
@@ -564,6 +596,22 @@
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>screen_room_selection_count</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%1$d message selected</string>
|
||||
<key>other</key>
|
||||
<string>%1$d messages selected</string>
|
||||
</dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@COUNT@</string>
|
||||
</dict>
|
||||
<key>screen_room_timeline_reaction_a11y</key>
|
||||
<dict>
|
||||
<key>COUNT</key>
|
||||
|
||||