Merge branch 'master' into ep/smp-server-pages

This commit is contained in:
Evgeny Poberezkin
2026-03-23 08:32:26 +00:00
504 changed files with 38814 additions and 2453 deletions
+95
View File
@@ -111,6 +111,7 @@ jobs:
arch: x86_64
runner: "ubuntu-22.04"
ghc: "8.10.7"
hash: 'sha256:5c8b2c0a6c745bc177669abfaa716b4bc57d58e2ea3882fb5da67f4d59e3dda5'
should_run: ${{ !(github.ref == 'refs/heads/stable' || startsWith(github.ref, 'refs/tags/v')) }}
- os: 22.04
os_underscore: 22_04
@@ -118,24 +119,28 @@ jobs:
runner: "ubuntu-22.04"
should_run: true
ghc: ${{ needs.variables.outputs.GHC_VER }}
hash: 'sha256:5c8b2c0a6c745bc177669abfaa716b4bc57d58e2ea3882fb5da67f4d59e3dda5'
- os: 24.04
os_underscore: 24_04
arch: x86_64
runner: "ubuntu-24.04"
should_run: true
ghc: ${{ needs.variables.outputs.GHC_VER }}
hash: 'sha256:98ff7968124952e719a8a69bb3cccdd217f5fe758108ac4f21ad22e1df44d237'
- os: 22.04
os_underscore: 22_04
arch: aarch64
runner: "ubuntu-22.04-arm"
should_run: true
ghc: ${{ needs.variables.outputs.GHC_VER }}
hash: 'sha256:6a62a4157b8775eaf4959cb629e757d32d39d1f4c8ac1b0ddc2510b555cf72f3'
- os: 24.04
os_underscore: 24_04
arch: aarch64
runner: "ubuntu-24.04-arm"
should_run: true
ghc: ${{ needs.variables.outputs.GHC_VER }}
hash: 'sha256:68434214381cb38287104e629fe8ee720167dd98cbb36ab1cbbab342515fa6ab'
steps:
- name: Checkout Code
if: matrix.should_run == true
@@ -182,6 +187,7 @@ jobs:
tags: build/${{ matrix.os }}:latest
build-args: |
TAG=${{ matrix.os }}
HASH=${{ matrix.hash }}
GHC=${{ matrix.ghc }}
USER_UID=${{ steps.ids.outputs.uid }}
USER_GID=${{ steps.ids.outputs.gid }}
@@ -582,3 +588,92 @@ jobs:
bin_hash: ${{ steps.windows_desktop_build.outputs.package_hash }}
github_ref: ${{ github.ref }}
github_token: ${{ secrets.GITHUB_TOKEN }}
# =========================
# NodeJS libs release
# =========================
# Downloads Desktop builds, extracts and archives libraries for NodeJS addon.
# Depends on Linux/MacOS, executes only on release.
# Secrets:
# -------
# NODEJS_REPO_TOKEN
# Only select repositories: simplex-chat-libs
# Permissions:
# * Contents (Read and Write)
release-nodejs-libs:
runs-on: ubuntu-latest
needs: [build-linux, build-macos]
if: startsWith(github.ref, 'refs/tags/v') && (!cancelled())
steps:
- name: Checkout current repository
uses: actions/checkout@v6
- name: Install packages for archiving
run: sudo apt install -y msitools gcc-mingw-w64
- name: Build archives
run: |
INIT_DIR='${{ runner.temp }}/artifacts'
RELEASE_DIR='${{ runner.temp }}/release-assets'
TAG='${{ github.ref_name }}'
URL='https://github.com/${{ github.repository }}/releases/download'
PREFIX='${{ github.event.repository.name }}-libs'
# Windows-specific
FILE_URL='https://raw.githubusercontent.com/${{ github.repository }}/refs/tags/${{ github.ref_name }}'
# Setup directories
mkdir "$INIT_DIR" "$RELEASE_DIR" && cd "$INIT_DIR"
# Downlaod desktop release
curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-ubuntu-22_04-x86_64.deb" -o linux.deb
curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-macos-aarch64.dmg" -o macos-aarch64.dmg
curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-macos-x86_64.dmg" -o macos-x86_64.dmg
curl --proto '=https' --tlsv1.2 -sSf -L "${URL}/${TAG}/simplex-desktop-windows-x86_64.msi" -o windows-x86_64.msi
# Linux
# -----
# Extract libraries
dpkg-deb -R linux.deb linux-out/ && cd linux-out/opt/simplex/lib/app/resources
# Preprare directory
mkdir libs && cp *.so libs/
# Archive
zip -r "${PREFIX}-linux-x86_64.zip" libs
# Back to original dir
mv "${PREFIX}-linux-x86_64.zip" "$RELEASE_DIR" && cd "$INIT_DIR"
# MacOS: aarch64
# --------------
7z x macos-aarch64.dmg -omacos1-out/ && cd macos1-out/SimpleX/SimpleX.app/Contents/app/resources/
mkdir libs && cp *.dylib libs/
zip -r "${PREFIX}-macos-aarch64.zip" libs
mv "${PREFIX}-macos-aarch64.zip" "$RELEASE_DIR" && cd "$INIT_DIR"
# Macos: x86_64
# -------------
7z x macos-x86_64.dmg -omacos2-out/ && cd macos2-out/SimpleX/SimpleX.app/Contents/app/resources/
mkdir libs && cp *.dylib libs/
zip -r "${PREFIX}-macos-x86_64.zip" libs
mv "${PREFIX}-macos-x86_64.zip" "$RELEASE_DIR" && cd "$INIT_DIR"
# Windows: x86_64
# ---------------
msiextract windows-x86_64.msi -C windows-out && cd windows-out/SimpleX/app/resources
# We need to generate library that exports symbols from Windows dll
curl --proto '=https' --tlsv1.2 -sSf -LO "${FILE_URL}/libsimplex.dll.def"
x86_64-w64-mingw32-dlltool -d libsimplex.dll.def -l libsimplex.lib -D libsimplex.dll
mkdir libs && cp *.dll *.lib libs/
zip -r "${PREFIX}-windows-x86_64.zip" libs
mv "${PREFIX}-windows-x86_64.zip" "$RELEASE_DIR" && cd "$INIT_DIR"
- name: Create release in libs repo and upload artifacts
uses: softprops/action-gh-release@v2
with:
repository: ${{ github.repository }}-libs
tag_name: ${{ github.ref_name }}
files: ${{ runner.temp }}/release-assets/*
token: ${{ secrets.NODEJS_REPO_TOKEN }}
+2
View File
@@ -55,6 +55,7 @@ website/src/img/images/
website/src/images/
website/src/js/lottie.min.js
website/src/js/ethers*
website/src/file-assets/
website/src/privacy.md
# Generated files
website/package/generated*
@@ -81,3 +82,4 @@ website/.cache
website/test/stubs-layout-cache/_includes/*.js
apps/android/app/release
apps/multiplatform/.kotlin/sessions
+2 -1
View File
@@ -1,6 +1,7 @@
# syntax=docker/dockerfile:1.7.0-labs
ARG TAG=24.04
FROM ubuntu:${TAG} AS build
ARG HASH=sha256:98ff7968124952e719a8a69bb3cccdd217f5fe758108ac4f21ad22e1df44d237
FROM ubuntu:${TAG}@${HASH} AS build
### Build stage
+4 -2
View File
@@ -12,6 +12,8 @@
[<img src="./images/trail-of-bits.jpg" height="80">](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) &nbsp;&nbsp;&nbsp; [<img src="./images/privacy-guides.jpg" height="64">](https://www.privacyguides.org/en/real-time-communication/#simplex-chat) &nbsp;&nbsp;&nbsp; [<img src="./images/whonix-logo.jpg" height="64">](https://www.whonix.org/wiki/Chat#Recommendation) &nbsp;&nbsp;&nbsp; [<img src="./images/kuketz-blog.jpg" height="64">](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/)
**[Why we are building SimpleX Network](./docs/WHY.md)**
## Welcome to SimpleX Chat!
1. 📲 [Install the app](#install-the-app).
@@ -42,7 +44,7 @@
## Connect to the team
You can connect to the team via the app using "chat with the developers button" available when you have no conversations in the profile, "Send questions and ideas" in the app settings or via our [SimpleX address](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). Please connect to:
You can connect to the team via the app using "chat with the developers button" available when you have no conversations in the profile, "Send questions and ideas" in the app settings or via our [SimpleX address](https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw). Please connect to:
- to ask any questions
- to suggest any improvements
@@ -54,7 +56,7 @@ If you are interested in helping us to integrate open-source language models, an
## Join user groups
You can find the groups created by users in [SimpleX Directory](https://simplex.chat/directory/). It is also available as [SimpleX bot](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) that allows to add your own groups and communities to the directory. We are not responsible for the content shared in these groups.
You can find the groups created by users in [SimpleX Directory](https://simplex.chat/directory/). It is also available as [SimpleX bot](https://smp4.simplex.im/a#lXUjJW5vHYQzoLYgmi8GbxkGP41_kjefFvBrdwg-0Ok) that allows to add your own groups and communities to the directory. We are not responsible for the content shared in these groups.
**Please note**: The groups below are created for the users to be able to ask questions, make suggestions and ask questions about SimpleX Chat only.
+219
View File
@@ -0,0 +1,219 @@
# Coding and building
You are an expert developer for SimpleX Chat, a privacy-first decentralized messaging platform. You MUST navigate and develop this codebase using the three-layer documentation architecture described below. You MUST NOT write code without first loading the relevant product and spec context.
## Three-Layer Documentation Architecture
### Why this structure exists
LLMs start each session with no persistent understanding of the codebase. Navigating thousands of lines of flat source code to reconstruct behavior, constraints, and intent wastes context window and produces unreliable results.
The `product/`, `spec/`, and source layers form a persistent, structured representation of the system that survives across sessions. Each layer is connected to the next by bidirectional cross-references. This structure enables you to load only the context relevant to a specific change, understand all affected concepts, and maintain coherence as the system evolves.
### The layers
| Layer | Contains | Question it answers |
|-------|----------|-------------------|
| `product/` | Capabilities, user flows, views, business rules, glossary | **What** does the system do and why? |
| `spec/` | Technical design, API contracts, database schema, service internals | **How** is it organized technically? |
| `Shared/`, `SimpleXChat/`, `SimpleX NSE/` | Executable Swift code (iOS app) | What does it **execute**? |
| `../../src/Simplex/Chat/` | Haskell core (chat logic, protocol, database) | What does the **core** execute? |
Each layer links to the next:
- `product/concepts.md` links every concept to its spec docs, source files, and tests in a single table — this is the primary navigation entry point
- `product/views/*.md` and `product/flows/*.md` each have a **Related spec:** line linking to their most relevant spec documents
- `product/glossary.md` uses *See: [spec/...]* references and `product/rules.md` uses **Spec:** [spec/...] references to link individual terms and rules down to spec
- `spec/` documents contain **Source:** headers and inline function links pointing down to source. Line references MUST be clickable by embedding the `#Lxx-Lyy` fragment in the link URL: [`functionName()`](Shared/Model/SimpleXAPI.swift#Lxx-Lyy). You MUST NOT duplicate line numbers in the display text — the URL fragment is sufficient. Why: redundant line numbers in display text create maintenance burden on every line shift.
- Reverse direction: the Document Map (end of this file) maps source → spec → product
### Navigation workflow
When the user requests any change, you MUST follow these steps before writing any code:
1. **Identify scope.** You MUST read `product/concepts.md` and find which product concepts are affected by the requested change. Each row links to the relevant product docs, spec docs, source files, and tests. Why: concepts.md is the fastest path to identify all affected documents — skipping it risks missing impacted areas.
2. **Load product context.** You MUST read the relevant `product/views/*.md` or `product/flows/*.md` to understand current user-facing behavior. For business constraints, you MUST read `product/rules.md`. Why: product documents define the intended behavior — changing code without understanding current behavior risks breaking the user contract.
3. **Load spec context.** You MUST follow the product → spec links to read the relevant `spec/*.md` or `spec/services/*.md`. You MUST understand the technical design, function signatures, and data flows. Why: spec documents reveal technical constraints and invariants that product docs omit — ignoring them leads to implementations that violate existing guarantees.
4. **Load source context.** You MUST follow the spec → source links (with line numbers) to read the relevant source files. Why: source code is the ground truth — product and spec may lag behind actual behavior.
5. **Identify full impact.** You MUST read `spec/impact.md` to find all product concepts affected by the source files you plan to change. This determines which documents you MUST update after the code change. Why: without impact analysis, documentation updates will be incomplete, and future sessions will navigate using stale information.
For internal-only changes that do not map to a product concept (infrastructure, refactoring, non-user-facing fixes), you MUST start at step 3 using the Document Map to find the relevant spec document, then proceed to steps 46.
6. **Implement.** Make the code change in source, then you MUST update all affected documentation as described in the Change Protocol below.
### Key navigation documents
| Document | Purpose | When to read |
|----------|---------|-------------|
| `product/concepts.md` | Concept → doc → code → test cross-reference | Starting point for every change |
| `product/rules.md` | Business invariants with enforcement locations and tests | Before modifying any behavior |
| `product/glossary.md` | Domain term definitions | When encountering unfamiliar terms |
| `product/gaps.md` | Known issues and recommendations | Before designing a fix or feature |
| `spec/impact.md` | Source file → affected product concepts | After identifying which files to change |
| Document Map (below) | Source ↔ spec ↔ product mapping | When updating documentation |
---
## Code Security
When designing code and planning implementations, you MUST:
- Apply adversarial thinking, and consider what may happen if one of the communicating parties is malicious. Why: security vulnerabilities arise from untested assumptions about trust boundaries.
- Formulate an explicit threat model for each change — who can do which undesirable things and under which circumstances. Why: explicit threat models catch attack vectors that implicit reasoning misses.
---
## Code Style
**Follow existing code patterns — you MUST:**
- Match the style of surrounding code. Why: consistent style reduces cognitive load and prevents unnecessary diff noise.
- Use Swift structs for value types, classes for reference types, and enums with associated values for variants. Why: correct type choices leverage the type system for compile-time correctness.
- Prefer exhaustive switch statements over default cases. Why: default cases bypass compiler checks for new enum cases and hide bugs.
**Comments policy — you MUST:**
- Only comment on non-obvious design decisions or tricky implementation details. Why: redundant comments create maintenance burden and drift from code.
- Keep function names and type signatures self-documenting. Why: good names eliminate the need for most comments.
- Assume a competent Swift reader. Why: over-explaining trivial Swift adds noise without value.
**Diff and refactoring — you MUST:**
- Avoid unnecessary changes and code movements. Why: unnecessary changes increase review burden and hide the meaningful diff.
- Never do refactoring unless it substantially reduces cost of solving the current problem, including the cost of refactoring itself. Why: speculative refactoring has guaranteed present cost with uncertain future benefit.
- Minimize the code changes — do what is minimally required to solve users' problems. Why: smaller diffs are easier to review, less likely to introduce bugs, and faster to revert.
**Document and code structure — you MUST:**
- **Never move existing code or sections around** — add new content at appropriate locations without reorganizing existing structure. Why: moving code creates large diffs that obscure the actual change and break git blame.
- When adding new sections to documents, continue the existing numbering scheme. Why: consistent numbering preserves document navigability.
- Minimize diff size — prefer small, targeted changes over reorganization. Why: large diffs compound review errors and make rollback difficult.
**Code analysis and review — you MUST:**
- Trace data flows end-to-end: from origin, through storage/parameters, to consumption. Flag values that are discarded and reconstructed from partial data (e.g. extracted from a URI missing original fields) — this is usually a bug. Why: broken data flows are the most common source of security and correctness bugs.
- Read implementations of called functions, not just signatures — if duplication involves a called function, check whether decomposing it resolves the duplication. Why: function signatures can be misleading about actual behavior.
- Read every function in the data flow even when the interface seems clear. Why: wrong assumptions about internals are the main source of missed bugs.
---
## Plans
When developing via plans (non-trivial features, multi-step changes, architectural decisions), you MUST store the plan in the `plans/` folder before implementing. Why: plans are the persistent record of design decisions and rationale — without them, future sessions cannot understand why the system was built the way it was.
### Plan requirements
1. **File naming.** You MUST use the format `YYYYMMDD_NN.md` (e.g., `20260211_01.md`). Why: chronological ordering makes it easy to trace the evolution of design decisions.
2. **Plan structure.** Every plan MUST include: (1) Problem statement, (2) Solution summary, (3) Detailed technical design, (4) Detailed implementation steps. Why: incomplete plans lead to ad-hoc implementation that drifts from intent.
3. **Consistency with product/ and spec/.** The plan MUST be consistent with the current state of `product/` and `spec/`. If the plan introduces new behavior, it MUST describe which product and spec documents will be affected. Why: plans that contradict existing documentation create conflicting sources of truth.
4. **Adversarial self-review.** After writing the plan, you MUST run the same adversarial self-review as for code changes: verify the plan is internally consistent, consistent with product/ and spec/, and does not introduce contradictions. You MUST repeat until two consecutive passes find zero issues. Why: an incoherent plan produces incoherent implementation.
---
## Change Protocol
### The rule
Every code change MUST include corresponding updates to `spec/` and `product/`. A task is NOT complete until all three layers are coherent with each other. Why: these layers are the persistent memory that enables coherent development across sessions — stale documentation creates false confidence and compounds errors in every future change.
### What to update
1. **spec/ — on every code change.** You MUST update the corresponding spec document to reflect the change. You MUST add new functions, update changed signatures, and remove deleted ones. Why: spec documents map 1:1 to source files — divergence defeats specification.
2. **product/ — when user-visible behavior changes.** You MUST update the relevant `product/views/*.md` and any affected `product/flows/*.md`. You MUST update `product/rules.md` when business invariants change. Why: product documents are the contract with users — silent changes create confusion.
3. **Line number references — on every code change.** You MUST verify and update all `#Lxx-Lyy` references in affected spec documents. Why: stale line numbers make spec documents misleading and destroy navigational value.
4. **Cross-references — when adding or removing files.** You MUST add corresponding spec documents and update `spec/README.md` document index and reverse index. When adding pages, you MUST add `product/views/` and `spec/client/` documents. You MUST update the Document Map at the end of this file. Why: every source file must be covered for the navigation system to work.
5. **Impact graph — when adding files or changing what a file affects.** You MUST update `spec/impact.md` to reflect the source file → product concept mapping. Why: the impact graph drives documentation updates for all future changes — an incomplete graph causes future changes to miss required updates.
6. **Concept index — when adding or changing product concepts.** You MUST add or update the relevant row in `product/concepts.md` with links to product docs, spec docs, source files, and tests. Why: the concept index is the entry point for all future navigation — a missing row means future changes to that concept will miss context.
7. **[GAP] annotations — when discovering issues.** When encountering missing error handling, dead code, inconsistencies, or incomplete features, you MUST add a `[GAP]` annotation in the relevant spec or product document and add a summary to `product/gaps.md`. Why: this builds institutional knowledge about technical debt.
8. **[REC] annotations — when identifying improvements.** You MUST add a `[REC]` annotation in the relevant document. Why: capturing improvement ideas at discovery time preserves context that is lost later.
9. **Preserve document structure.** You MUST follow existing format conventions: spec documents use function-anchored links with line numbers, product documents use interaction descriptions, flow documents use Mermaid diagrams. Why: consistent structure makes documents predictable and navigable.
### Adversarial self-review
After completing all changes (code + documentation), you MUST run an adversarial self-review. You MUST check coherence both within each layer and across layers.
**Within-layer coherence — you MUST verify:**
- spec/ is internally consistent — no contradictory descriptions, state machines have no unreachable states, data model is referentially intact
- product/ is internally consistent — flows match views, rules match behavior descriptions
**Across-layer coherence — you MUST verify:**
- Every new or changed function in source appears in the corresponding spec/ document
- Every user-visible behavior change in source appears in the relevant product/ document
- All `#Lxx-Lyy` line references in affected spec documents point to the correct lines
- All cross-references resolve — product → spec links, spec → source links
- `spec/impact.md` covers all affected product concepts for the changed source files
- `product/concepts.md` rows are current for any affected concepts
**Convergence:** You MUST repeat the review-and-fix cycle until two consecutive passes find zero issues. You MUST fix all issues discovered between passes. Why: LLM non-determinism means a single review pass may miss violations — two consecutive clean passes provide confidence that the layers are coherent.
---
## Document Map
### iOS Swift Sources
| Source Location | Spec Document | Product Document |
|----------------|---------------|-----------------|
| Shared/ContentView.swift | spec/client/navigation.md | product/views/chat-list.md |
| Shared/SimpleXApp.swift | spec/architecture.md | product/flows/onboarding.md |
| Shared/AppDelegate.swift | spec/services/notifications.md | product/flows/onboarding.md |
| Shared/Views/ChatList/ChatListView.swift | spec/client/chat-list.md | product/views/chat-list.md |
| Shared/Views/Chat/ChatView.swift | spec/client/chat-view.md | product/views/chat.md |
| Shared/Views/Chat/ComposeMessage/ComposeView.swift | spec/client/compose.md | product/views/chat.md |
| Shared/Views/Chat/ChatItem/ | spec/client/chat-view.md | product/views/chat.md |
| Shared/Views/Chat/ChatInfoView.swift | spec/client/chat-view.md | product/views/contact-info.md |
| Shared/Views/Chat/Group/GroupChatInfoView.swift | spec/client/chat-view.md | product/views/group-info.md |
| Shared/Views/Chat/Group/AddGroupMembersView.swift | spec/client/chat-view.md | product/views/group-info.md |
| Shared/Views/Chat/Group/GroupLinkView.swift | spec/client/chat-view.md | product/views/group-info.md |
| Shared/Views/Chat/Group/GroupMemberInfoView.swift | spec/client/chat-view.md | product/views/group-info.md |
| Shared/Views/NewChat/NewChatView.swift | spec/client/navigation.md | product/views/new-chat.md |
| Shared/Views/NewChat/QRCode.swift | spec/client/navigation.md | product/views/new-chat.md |
| Shared/Views/Call/ActiveCallView.swift | spec/services/calls.md | product/views/call.md |
| Shared/Views/Call/CallController.swift | spec/services/calls.md | product/flows/calling.md |
| Shared/Views/Call/WebRTCClient.swift | spec/services/calls.md | product/flows/calling.md |
| Shared/Views/UserSettings/SettingsView.swift | spec/client/navigation.md | product/views/settings.md |
| Shared/Views/UserSettings/AppearanceSettings.swift | spec/services/theme.md | product/views/settings.md |
| Shared/Views/UserSettings/NetworkAndServers/ | spec/architecture.md | product/views/settings.md |
| Shared/Views/UserSettings/UserProfilesView.swift | spec/client/navigation.md | product/views/user-profiles.md |
| Shared/Views/Onboarding/ | spec/client/navigation.md | product/views/onboarding.md |
| Shared/Views/LocalAuth/ | spec/architecture.md | product/views/settings.md |
| Shared/Views/Database/ | spec/database.md | product/views/settings.md |
| Shared/Views/Migration/ | spec/database.md | product/flows/onboarding.md |
| Shared/Model/ChatModel.swift | spec/state.md | product/concepts.md |
| Shared/Model/SimpleXAPI.swift | spec/api.md, spec/architecture.md | product/concepts.md |
| Shared/Model/AppAPITypes.swift | spec/api.md | product/concepts.md |
| Shared/Model/NtfManager.swift | spec/services/notifications.md | product/flows/messaging.md |
| Shared/Model/BGManager.swift | spec/services/notifications.md | product/flows/messaging.md |
| Shared/Theme/ThemeManager.swift | spec/services/theme.md | product/views/settings.md |
| SimpleXChat/ChatTypes.swift | spec/state.md, spec/api.md | product/glossary.md |
| SimpleXChat/APITypes.swift | spec/api.md | product/concepts.md |
| SimpleXChat/CallTypes.swift | spec/services/calls.md | product/flows/calling.md |
| SimpleXChat/FileUtils.swift | spec/services/files.md | product/flows/file-transfer.md |
| SimpleXChat/Notifications.swift | spec/services/notifications.md | product/flows/messaging.md |
| SimpleX NSE/NotificationService.swift | spec/services/notifications.md | product/flows/messaging.md |
### Haskell Core Sources (at `../../src/Simplex/Chat/` relative to `apps/ios/`)
| Source Location | Spec Document | Product Document |
|----------------|---------------|-----------------|
| ../../src/Simplex/Chat/Controller.hs | spec/api.md | product/concepts.md |
| ../../src/Simplex/Chat/Types.hs | spec/api.md | product/glossary.md |
| ../../src/Simplex/Chat/Core.hs | spec/architecture.md | product/concepts.md |
| ../../src/Simplex/Chat/Protocol.hs | spec/architecture.md | product/concepts.md |
| ../../src/Simplex/Chat/Messages.hs | spec/api.md | product/flows/messaging.md |
| ../../src/Simplex/Chat/Messages/CIContent.hs | spec/api.md | product/flows/messaging.md |
| ../../src/Simplex/Chat/Call.hs | spec/services/calls.md | product/flows/calling.md |
| ../../src/Simplex/Chat/Files.hs | spec/services/files.md | product/flows/file-transfer.md |
| ../../src/Simplex/Chat/Store/Messages.hs | spec/database.md | product/flows/messaging.md |
| ../../src/Simplex/Chat/Store/Groups.hs | spec/database.md | product/flows/group-lifecycle.md |
| ../../src/Simplex/Chat/Store/Direct.hs | spec/database.md | product/flows/connection.md |
| ../../src/Simplex/Chat/Store/Files.hs | spec/database.md | product/flows/file-transfer.md |
| ../../src/Simplex/Chat/Store/Profiles.hs | spec/database.md | product/views/user-profiles.md |
+92
View File
@@ -1 +1,93 @@
# SimpleX Chat iOS app
This file provides guidance when working with code in this repository.
## iOS App Overview
The iOS app is a SwiftUI application that interfaces with the Haskell core library via FFI. It shares the SimpleXChat framework with two extensions: Notification Service Extension (NSE) for push notifications and Share Extension (SE) for sharing content from other apps.
## Build & Development
Open `SimpleX.xcodeproj` in Xcode. The project has five targets:
- **SimpleX (iOS)** - Main app (Bundle ID: `chat.simplex.app`)
- **SimpleXChat** - Framework containing FFI bridge and shared types
- **SimpleX NSE** - Notification Service Extension
- **SimpleX SE** - Share Extension
- **Tests iOS** - UI tests
Build and run via Xcode (Product > Build/Run). Tests run via Product > Test or:
```bash
xcodebuild test -scheme "SimpleX (iOS)" -destination 'platform=iOS Simulator,name=iPhone 15'
```
Deployment target: iOS 15.0+, Swift 5.0.
## Architecture
### Haskell Core Integration
The app calls the Haskell core library through C FFI defined in `SimpleXChat/SimpleX.h`:
- `chat_migrate_init_key()` - Initialize/migrate database
- `chat_send_cmd_retry()` - Send command to chat controller
- `chat_recv_msg_wait()` - Receive messages from controller
Swift wrappers in `SimpleXChat/API.swift`:
- `chatMigrateInit()` - Initialize chat controller
- `sendSimpleXCmd<R>()` - Send typed commands and parse responses
- `recvSimpleXMsg<R>()` - Receive typed messages
Haskell runtime initialization (`SimpleXChat/hs_init.c`) uses different memory configurations:
- Main app: 64MB heap
- NSE: 512KB heap (minimal footprint for background processing)
- SE: 1MB heap
Pre-compiled Haskell libraries are in `Libraries/{ios,mac,sim}/`.
### State Management
- **ChatModel** (`Shared/Model/ChatModel.swift`) - Main singleton `ObservableObject` for app-wide state (chat list, active chat, users)
- **ItemsModel** - Manages chat items within a selected chat (similar to Kotlin's ChatsContext)
- **AppTheme** - Theme management and customization
### App Structure
Entry point: `Shared/SimpleXApp.swift`
Key directories in `Shared/`:
- `Model/` - Data models and API layer (`ChatModel.swift`, `SimpleXAPI.swift`)
- `Views/` - SwiftUI views organized by feature:
- `ChatList/` - Chat list and user picker
- `Chat/` - Message display and composition
- `Call/` - VoIP call UI
- `UserSettings/` - App settings
- `LocalAuth/` - Passcode and biometric authentication
- `Database/` - Database initialization and migration
### Shared Data Between Targets
All three targets share data via App Group (`group.chat.simplex.app`):
- `SimpleXChat/AppGroup.swift` - GroupDefaults wrapper for typed shared preferences
- Keychain for sensitive data: `kcDatabasePassword`, `kcAppPassword`, `kcSelfDestructPassword`
### Key Types
Types are defined in `SimpleXChat/`:
- `ChatTypes.swift` - User, Chat, Message, Group types
- `APITypes.swift` - API request/response types
Commands follow `ChatCmdProtocol` (has `cmdString` property), sent as JSON through FFI.
## Localization
31 languages supported. Localization files in `SimpleX Localizations/`.
Workflow:
- `Product > Export Localizations` - Export XLIFF files
- `Product > Import Localizations` - Import updated translations
## Background Capabilities
Configured in Info.plist:
- Background modes: audio, fetch, remote-notification, voip
- URL scheme: `simplex://` for deep linking
- BGTaskScheduler: `chat.simplex.app.receive`
+1
View File
@@ -5,6 +5,7 @@
// Created by Evgeny on 30/03/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/services/notifications.md
import Foundation
import UIKit
+10
View File
@@ -4,6 +4,7 @@
//
// Created by Evgeny Poberezkin on 17/01/2022.
//
// Spec: spec/client/navigation.md
import SwiftUI
import Intents
@@ -19,15 +20,18 @@ private enum NoticesSheet: Identifiable {
}
}
// Spec: spec/client/navigation.md#ContentView
struct ContentView: View {
@EnvironmentObject var chatModel: ChatModel
@ObservedObject var alertManager = AlertManager.shared
@ObservedObject var callController = CallController.shared
// Spec: spec/client/navigation.md#AppSheetState
@ObservedObject var appSheetState = AppSheetState.shared
@Environment(\.colorScheme) var colorScheme
@EnvironmentObject var theme: AppTheme
@EnvironmentObject var sceneDelegate: SceneDelegate
// Spec: spec/client/navigation.md#contentAccessAuthenticationExtended
var contentAccessAuthenticationExtended: Bool
@Environment(\.scenePhase) var scenePhase
@@ -161,6 +165,7 @@ struct ContentView: View {
}
}
// Spec: spec/client/navigation.md#contentView
@ViewBuilder private func contentView() -> some View {
if let status = chatModel.chatDbStatus, status != .ok {
DatabaseErrorView(status: status)
@@ -176,6 +181,7 @@ struct ContentView: View {
}
}
// Spec: spec/client/navigation.md#callView
@ViewBuilder private func callView(_ call: Call) -> some View {
if CallController.useCallKit() {
ActiveCallView(call: call, canConnectCall: Binding.constant(true))
@@ -193,6 +199,7 @@ struct ContentView: View {
}
}
// Spec: spec/client/navigation.md#callBanner
private func activeCallInteractiveArea(_ call: Call) -> some View {
HStack {
Text(call.contact.displayName).font(.body).foregroundColor(.white)
@@ -227,6 +234,7 @@ struct ContentView: View {
}
}
// Spec: spec/client/navigation.md#lockButton
private func lockButton() -> some View {
Button(action: authenticateContentViewAccess) { Label("Unlock", systemImage: "lock") }
}
@@ -339,6 +347,7 @@ struct ContentView: View {
}
}
// Spec: spec/client/navigation.md#unlockedRecently
private func unlockedRecently() -> Bool {
if let lastSuccessfulUnlock = lastSuccessfulUnlock {
return ProcessInfo.processInfo.systemUptime - lastSuccessfulUnlock < 2
@@ -426,6 +435,7 @@ struct ContentView: View {
)
}
// Spec: spec/client/navigation.md#connectViaUrl
func connectViaUrl() {
let m = ChatModel.shared
if let url = m.appOpenUrl {
+15 -3
View File
@@ -5,11 +5,13 @@
// Created by EP on 01/05/2025.
// Copyright © 2025 SimpleX Chat. All rights reserved.
//
// Spec: spec/api.md
import SimpleXChat
import SwiftUI
// some constructors are used in SEChatCommand or NSEChatCommand types as well - they must be syncronised
// Spec: spec/api.md#ChatCommand
enum ChatCommand: ChatCmdProtocol {
case showActiveUser
case createActiveUser(profile: Profile?, pastTimestamp: Bool)
@@ -41,6 +43,7 @@ enum ChatCommand: ChatCmdProtocol {
case apiGetChatTags(userId: Int64)
case apiGetChats(userId: Int64)
case apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTag?, pagination: ChatPagination, search: String)
case apiGetChatContentTypes(chatId: ChatId, scope: GroupChatScope?)
case apiGetChatItemInfo(type: ChatType, id: Int64, scope: GroupChatScope?, itemId: Int64)
case apiSendMessages(type: ChatType, id: Int64, scope: GroupChatScope?, live: Bool, ttl: Int?, composedMessages: [ComposedMessage])
case apiCreateChatTag(tag: ChatTagData)
@@ -224,7 +227,8 @@ enum ChatCommand: ChatCmdProtocol {
case let .apiGetChats(userId): return "/_get chats \(userId) pcc=on"
case let .apiGetChat(chatId, scope, contentTag, pagination, search):
let tag = contentTag != nil ? " content=\(contentTag!.rawValue)" : ""
return "/_get chat \(chatId)\(scopeRef(scope: scope))\(tag) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)")
return "/_get chat \(chatId)\(scopeRef(scope))\(tag) \(pagination.cmdString)" + (search == "" ? "" : " search=\(search)")
case let .apiGetChatContentTypes(chatId, scope): return "/_get content types \(chatId)\(scopeRef(scope))"
case let .apiGetChatItemInfo(type, id, scope, itemId): return "/_get item info \(ref(type, id, scope: scope)) \(itemId)"
case let .apiSendMessages(type, id, scope, live, ttl, composedMessages):
let msgs = encodeJSON(composedMessages)
@@ -417,6 +421,7 @@ enum ChatCommand: ChatCmdProtocol {
case .apiGetChatTags: return "apiGetChatTags"
case .apiGetChats: return "apiGetChats"
case .apiGetChat: return "apiGetChat"
case .apiGetChatContentTypes: return "apiGetChatContentTypes"
case .apiGetChatItemInfo: return "apiGetChatItemInfo"
case .apiSendMessages: return "apiSendMessages"
case .apiCreateChatTag: return "apiCreateChatTag"
@@ -559,10 +564,10 @@ enum ChatCommand: ChatCmdProtocol {
}
func ref(_ type: ChatType, _ id: Int64, scope: GroupChatScope?) -> String {
"\(type.rawValue)\(id)\(scopeRef(scope: scope))"
"\(type.rawValue)\(id)\(scopeRef(scope))"
}
func scopeRef(scope: GroupChatScope?) -> String {
func scopeRef(_ scope: GroupChatScope?) -> String {
switch (scope) {
case .none: ""
case let .memberSupport(groupMemberId_):
@@ -640,6 +645,7 @@ enum ChatCommand: ChatCmdProtocol {
}
// ChatResponse is split to three enums to reduce stack size used when parsing it, parsing large enums is very inefficient.
// Spec: spec/api.md#ChatResponse0
enum ChatResponse0: Decodable, ChatAPIResult {
case activeUser(user: User)
case usersList(users: [UserInfo])
@@ -648,6 +654,7 @@ enum ChatResponse0: Decodable, ChatAPIResult {
case chatStopped
case apiChats(user: UserRef, chats: [ChatData])
case apiChat(user: UserRef, chat: ChatData, navInfo: NavigationInfo?)
case chatContentTypes(contentTypes: [MsgContentTag])
case chatTags(user: UserRef, userTags: [ChatTag])
case chatItemInfo(user: UserRef, chatItem: AChatItem, chatItemInfo: ChatItemInfo)
case serverTestResult(user: UserRef, testServer: String, testFailure: ProtocolTestFailure?)
@@ -680,6 +687,7 @@ enum ChatResponse0: Decodable, ChatAPIResult {
case .chatStopped: "chatStopped"
case .apiChats: "apiChats"
case .apiChat: "apiChat"
case .chatContentTypes: "chatContentTypes"
case .chatTags: "chatTags"
case .chatItemInfo: "chatItemInfo"
case .serverTestResult: "serverTestResult"
@@ -714,6 +722,7 @@ enum ChatResponse0: Decodable, ChatAPIResult {
case .chatStopped: return noDetails
case let .apiChats(u, chats): return withUser(u, String(describing: chats))
case let .apiChat(u, chat, navInfo): return withUser(u, "chat: \(String(describing: chat))\nnavInfo: \(String(describing: navInfo))")
case let .chatContentTypes(types): return "content types: \(String(describing: types))"
case let .chatTags(u, userTags): return withUser(u, "userTags: \(String(describing: userTags))")
case let .chatItemInfo(u, chatItem, chatItemInfo): return withUser(u, "chatItem: \(String(describing: chatItem))\nchatItemInfo: \(String(describing: chatItemInfo))")
case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))")
@@ -758,6 +767,7 @@ enum ChatResponse0: Decodable, ChatAPIResult {
}
}
// Spec: spec/api.md#ChatResponse1
enum ChatResponse1: Decodable, ChatAPIResult {
case invitation(user: UserRef, connLinkInvitation: CreatedConnLink, connection: PendingContactConnection)
case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection)
@@ -897,6 +907,7 @@ enum ChatResponse1: Decodable, ChatAPIResult {
}
}
// Spec: spec/api.md#ChatResponse2
enum ChatResponse2: Decodable, ChatAPIResult {
// group responses
case groupCreated(user: UserRef, groupInfo: GroupInfo)
@@ -1040,6 +1051,7 @@ enum ChatResponse2: Decodable, ChatAPIResult {
}
}
// Spec: spec/api.md#ChatEvent
enum ChatEvent: Decodable, ChatAPIResult {
case chatSuspended
case contactSwitch(user: UserRef, contact: Contact, switchProgress: SwitchProgress)
+6
View File
@@ -5,6 +5,7 @@
// Created by Evgeny Poberezkin on 08/02/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/services/notifications.md
import Foundation
import BackgroundTasks
@@ -25,6 +26,7 @@ private let maxBgRefreshInterval: TimeInterval = 2400 // 40 minutes
private let maxTimerCount = 9
// Spec: spec/services/notifications.md#BGManager
class BGManager {
static let shared = BGManager()
var chatReceiver: ChatReceiver?
@@ -32,6 +34,7 @@ class BGManager {
var completed = true
var timerCount = 0
// Spec: spec/services/notifications.md#register
func register() {
logger.debug("BGManager.register")
BGTaskScheduler.shared.register(forTaskWithIdentifier: receiveTaskId, using: nil) { task in
@@ -39,6 +42,7 @@ class BGManager {
}
}
// Spec: spec/services/notifications.md#schedule
func schedule(interval: TimeInterval? = nil) {
if !ChatModel.shared.ntfEnableLocal {
logger.debug("BGManager.schedule: disabled")
@@ -66,6 +70,7 @@ class BGManager {
Date.now.timeIntervalSince(chatLastBackgroundRunGroupDefault.get()) > runInterval
}
// Spec: spec/services/notifications.md#handleRefresh
private func handleRefresh(_ task: BGAppRefreshTask) {
if !ChatModel.shared.ntfEnableLocal {
logger.debug("BGManager.handleRefresh: disabled")
@@ -103,6 +108,7 @@ class BGManager {
}
}
// Spec: spec/services/notifications.md#receiveMessages-BG
func receiveMessages(_ completeReceiving: @escaping (String) -> Void) {
if (!self.completed) {
logger.debug("BGManager.receiveMessages: in progress, exiting")
+20
View File
@@ -5,6 +5,7 @@
// Created by Evgeny Poberezkin on 22/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/state.md
import Foundation
import Combine
@@ -53,6 +54,7 @@ private func addTermItem(_ items: inout [TerminalItem], _ item: TerminalItem) {
}
// analogue for SecondaryContextFilter in Kotlin
// Spec: spec/state.md#SecondaryItemsModelFilter
enum SecondaryItemsModelFilter {
case groupChatScopeContext(groupScopeInfo: GroupChatScopeInfo)
case msgContentTagContext(contentTag: MsgContentTag)
@@ -68,6 +70,7 @@ enum SecondaryItemsModelFilter {
}
// analogue for ChatsContext in Kotlin
// Spec: spec/state.md#ItemsModel
class ItemsModel: ObservableObject {
static let shared = ItemsModel(secondaryIMFilter: nil)
public var secondaryIMFilter: SecondaryItemsModelFilter?
@@ -103,12 +106,14 @@ class ItemsModel: ObservableObject {
.store(in: &bag)
}
// Spec: spec/state.md#loadSecondaryChat
static func loadSecondaryChat(_ chatId: ChatId, chatFilter: SecondaryItemsModelFilter, willNavigate: @escaping () -> Void = {}) {
let im = ItemsModel(secondaryIMFilter: chatFilter)
ChatModel.shared.secondaryIM = im
im.loadOpenChat(chatId, willNavigate: willNavigate)
}
// Spec: spec/state.md#loadOpenChat
func loadOpenChat(_ chatId: ChatId, willNavigate: @escaping () -> Void = {}) {
navigationTimeoutTask?.cancel()
loadChatTask?.cancel()
@@ -134,6 +139,7 @@ class ItemsModel: ObservableObject {
}
}
// Spec: spec/state.md#loadOpenChatNoWait
func loadOpenChatNoWait(_ chatId: ChatId, _ openAroundItemId: ChatItem.ID? = nil) {
navigationTimeoutTask?.cancel()
loadChatTask?.cancel()
@@ -179,6 +185,7 @@ class PreloadState {
}
}
// Spec: spec/state.md#ChatTagsModel
class ChatTagsModel: ObservableObject {
static let shared = ChatTagsModel()
@@ -326,6 +333,7 @@ class ConnectProgressManager: ObservableObject {
}
}
// Spec: spec/state.md#ChatModel
final class ChatModel: ObservableObject {
@Published var onboardingStage: OnboardingStage?
@Published var setDeliveryReceipts = false
@@ -383,6 +391,7 @@ final class ChatModel: ObservableObject {
@Published var showCallView = false
@Published var activeCallViewIsCollapsed = false
// remote desktop
// Spec: spec/architecture.md#remoteCtrlSession
@Published var remoteCtrlSession: RemoteCtrlSession?
// currently showing invitation
@Published var showingInvitation: ShowingInvitation?
@@ -423,6 +432,7 @@ final class ChatModel: ObservableObject {
userAddress?.shortLinkDataSet ?? true
}
// Spec: spec/state.md#getUser
func getUser(_ userId: Int64) -> User? {
currentUser?.userId == userId
? currentUser
@@ -433,6 +443,7 @@ final class ChatModel: ObservableObject {
users.firstIndex { $0.user.userId == user.userId }
}
// Spec: spec/state.md#updateUser
func updateUser(_ user: User) {
if let i = getUserIndex(user) {
users[i].user = user
@@ -442,6 +453,7 @@ final class ChatModel: ObservableObject {
}
}
// Spec: spec/state.md#removeUser
func removeUser(_ user: User) {
if let i = getUserIndex(user) {
users.remove(at: i)
@@ -452,6 +464,7 @@ final class ChatModel: ObservableObject {
chats.first(where: { $0.id == id }) != nil
}
// Spec: spec/state.md#getChat
func getChat(_ id: String) -> Chat? {
chats.first(where: { $0.id == id })
}
@@ -506,6 +519,7 @@ final class ChatModel: ObservableObject {
chats.firstIndex(where: { $0.id == id })
}
// Spec: spec/state.md#addChat
func addChat(_ chat: Chat) {
if chatId == nil {
withAnimation { addChat_(chat, at: 0) }
@@ -519,6 +533,7 @@ final class ChatModel: ObservableObject {
chats.insert(chat, at: position)
}
// Spec: spec/state.md#updateChatInfo
func updateChatInfo(_ cInfo: ChatInfo) {
if let i = getChatIndex(cInfo.id) {
if case let .group(groupInfo, groupChatScope) = cInfo, groupChatScope != nil {
@@ -570,6 +585,7 @@ final class ChatModel: ObservableObject {
}
}
// Spec: spec/state.md#replaceChat
func replaceChat(_ id: String, _ chat: Chat) {
if let i = getChatIndex(id) {
chats[i] = chat
@@ -1054,6 +1070,7 @@ final class ChatModel: ObservableObject {
NtfManager.shared.changeNtfBadgeCount(by: by)
}
// Spec: spec/state.md#totalUnreadCountForAllUsers
func totalUnreadCountForAllUsers() -> Int {
var unread: Int = 0
for chat in chats {
@@ -1153,6 +1170,7 @@ final class ChatModel: ObservableObject {
return (prevMember, memberIds.count)
}
// Spec: spec/state.md#popChat
func popChat(_ id: String) {
if let i = getChatIndex(id) {
// no animation here, for it not to look like it just moved when leaving the chat
@@ -1176,6 +1194,7 @@ final class ChatModel: ObservableObject {
showingInvitation?.connChatUsed = true
}
// Spec: spec/state.md#removeChat
func removeChat(_ id: String) {
withAnimation {
if let i = getChatIndex(id) {
@@ -1248,6 +1267,7 @@ struct NTFContactRequest {
var chatId: String
}
// Spec: spec/state.md#Chat
final class Chat: ObservableObject, Identifiable, ChatLike {
@Published var chatInfo: ChatInfo
@Published var chatItems: [ChatItem]
+10
View File
@@ -5,6 +5,7 @@
// Created by Evgeny Poberezkin on 08/02/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/services/notifications.md
import Foundation
import UserNotifications
@@ -22,6 +23,7 @@ enum NtfCallAction {
case reject
}
// Spec: spec/services/notifications.md#NtfManager
class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
static let shared = NtfManager()
@@ -48,6 +50,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
handler()
}
// Spec: spec/services/notifications.md#processNotificationResponse
func processNotificationResponse(_ ntfResponse: UNNotificationResponse) {
let chatModel = ChatModel.shared
let content = ntfResponse.notification.request.content
@@ -149,6 +152,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
return false
}
// Spec: spec/services/notifications.md#registerCategories
func registerCategories() {
logger.debug("NtfManager.registerCategories")
UNUserNotificationCenter.current().setNotificationCategories([
@@ -207,6 +211,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
])
}
// Spec: spec/services/notifications.md#requestAuthorization
func requestAuthorization(onDeny denied: (()-> Void)? = nil, onAuthorized authorized: (()-> Void)? = nil) {
logger.debug("NtfManager.requestAuthorization")
let center = UNUserNotificationCenter.current()
@@ -230,6 +235,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
}
}
// Spec: spec/services/notifications.md#notifyContactRequest
func notifyContactRequest(_ user: any UserLike, _ contactRequest: UserContactRequest) {
logger.debug("NtfManager.notifyContactRequest")
addNotification(createContactRequestNtf(user, contactRequest, 0))
@@ -240,6 +246,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
addNotification(createContactConnectedNtf(user, contact, 0))
}
// Spec: spec/services/notifications.md#notifyMessageReceived
func notifyMessageReceived(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) {
logger.debug("NtfManager.notifyMessageReceived")
if cInfo.ntfsEnabled(chatItem: cItem) {
@@ -247,16 +254,19 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
}
}
// Spec: spec/services/notifications.md#notifyCallInvitation
func notifyCallInvitation(_ invitation: RcvCallInvitation) {
logger.debug("NtfManager.notifyCallInvitation")
addNotification(createCallInvitationNtf(invitation, 0))
}
// Spec: spec/services/notifications.md#setNtfBadgeCount
func setNtfBadgeCount(_ count: Int) {
UIApplication.shared.applicationIconBadgeNumber = count
ntfBadgeCountGroupDefault.set(count)
}
// Spec: spec/services/notifications.md#changeNtfBadgeCount
func changeNtfBadgeCount(by count: Int = 1) {
setNtfBadgeCount(max(0, UIApplication.shared.applicationIconBadgeNumber + count))
}
+30 -4
View File
@@ -5,6 +5,7 @@
// Created by Evgeny Poberezkin on 27/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/api.md | spec/architecture.md
import Foundation
import UIKit
@@ -49,6 +50,7 @@ enum TerminalItem: Identifiable {
}
}
// Spec: spec/architecture.md#beginBGTask
func beginBGTask(_ handler: (() -> Void)? = nil) -> (() -> Void) {
var id: UIBackgroundTaskIdentifier!
var running = true
@@ -86,12 +88,14 @@ private func withBGTask<T>(bgDelay: Double? = nil, f: @escaping () -> T) -> T {
return r
}
// Spec: spec/api.md#chatSendCmdSync
@inline(__always)
func chatSendCmdSync<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, log: Bool = true) throws -> R {
let res: APIResult<R> = chatApiSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl: ctrl, log: log)
return try apiResult(res)
}
// Spec: spec/api.md#chatApiSendCmdSync
func chatApiSendCmdSync<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, retryNum: Int32 = 0, log: Bool = true) -> APIResult<R> {
if log {
logger.debug("chatSendCmd \(cmd.cmdType)")
@@ -112,12 +116,14 @@ func chatApiSendCmdSync<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = tru
return resp
}
// Spec: spec/api.md#chatSendCmd
@inline(__always)
func chatSendCmd<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, log: Bool = true) async throws -> R {
let res: APIResult<R> = await chatApiSendCmd(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl: ctrl, log: log)
return try apiResult(res)
}
// Spec: spec/api.md#chatApiSendCmdWithRetry
func chatApiSendCmdWithRetry<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, inProgress: BoxedValue<Bool>? = nil, retryNum: Int32 = 0) async -> APIResult<R>? {
let r: APIResult<R> = await chatApiSendCmd(cmd, bgTask: bgTask, bgDelay: bgDelay, retryNum: retryNum)
if inProgress == nil || inProgress?.boxedValue == true,
@@ -210,6 +216,7 @@ func proxyDestinationErrorAlertMessage(proxyServer: String, destServer: String)
String.localizedStringWithFormat(NSLocalizedString("Forwarding server %@ failed to connect to destination server %@. Please try later.", comment: "alert message"), serverHostname(proxyServer), serverHostname(destServer))
}
// Spec: spec/api.md#chatApiSendCmd
@inline(__always)
func chatApiSendCmd<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, retryNum: Int32 = 0, log: Bool = true) async -> APIResult<R> {
await withCheckedContinuation { cont in
@@ -226,6 +233,7 @@ func apiResult<R: ChatAPIResult>(_ res: APIResult<R>) throws -> R {
}
}
// Spec: spec/api.md#chatRecvMsg
func chatRecvMsg(_ ctrl: chat_ctrl? = nil) async -> APIResult<ChatEvent>? {
await withCheckedContinuation { cont in
_ = withBGTask(bgDelay: msgDelay) { () -> APIResult<ChatEvent>? in
@@ -346,6 +354,7 @@ func apiStopChat() async throws {
}
}
// Spec: spec/architecture.md#apiActivateChat
func apiActivateChat() {
chatReopenStore()
do {
@@ -355,6 +364,7 @@ func apiActivateChat() {
}
}
// Spec: spec/architecture.md#apiSuspendChat
func apiSuspendChat(timeoutMicroseconds: Int) {
do {
try sendCommandOkRespSync(.apiSuspendChat(timeoutMicroseconds: timeoutMicroseconds))
@@ -363,12 +373,14 @@ func apiSuspendChat(timeoutMicroseconds: Int) {
}
}
// Spec: spec/services/files.md#apiSetAppFilePaths
func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String, ctrl: chat_ctrl? = nil) throws {
let r: ChatResponse2 = try chatSendCmdSync(.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder), ctrl: ctrl)
if case .cmdOk = r { return }
throw r.unexpected
}
// Spec: spec/services/files.md#apiSetEncryptLocalFiles
func apiSetEncryptLocalFiles(_ enable: Bool) throws {
try sendCommandOkRespSync(.apiSetEncryptLocalFiles(enable: enable))
}
@@ -444,11 +456,17 @@ func apiGetChat(chatId: ChatId, scope: GroupChatScope?, contentTag: MsgContentTa
throw r.unexpected
}
func loadChat(chat: Chat, im: ItemsModel, search: String = "", clearItems: Bool = true) async {
await loadChat(chatId: chat.chatInfo.id, im: im, search: search, clearItems: clearItems)
func apiGetChatContentTypes(chatId: ChatId, scope: GroupChatScope? = nil) async throws -> [MsgContentTag] {
let r: ChatResponse0 = try await chatSendCmd(.apiGetChatContentTypes(chatId: chatId, scope: scope))
if case let .chatContentTypes(types) = r { return types }
throw r.unexpected
}
func loadChat(chatId: ChatId, im: ItemsModel, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async {
func loadChat(chat: Chat, im: ItemsModel, contentTag: MsgContentTag? = nil, search: String = "", clearItems: Bool = true) async {
await loadChat(chatId: chat.chatInfo.id, im: im, contentTag: contentTag, search: search, clearItems: clearItems)
}
func loadChat(chatId: ChatId, im: ItemsModel, contentTag: MsgContentTag? = nil, search: String = "", openAroundItemId: ChatItem.ID? = nil, clearItems: Bool = true) async {
await MainActor.run {
if clearItems {
im.reversedChatItems = []
@@ -462,10 +480,11 @@ func loadChat(chatId: ChatId, im: ItemsModel, search: String = "", openAroundIte
openAroundItemId != nil
? .around(chatItemId: openAroundItemId!, count: loadItemsPerPage)
: (
search == ""
contentTag == nil && search == ""
? .initial(count: loadItemsPerPage) : .last(count: loadItemsPerPage)
)
),
contentTag,
search,
openAroundItemId,
{ 0...0 }
@@ -1448,6 +1467,7 @@ func standaloneFileInfo(url: String, ctrl: chat_ctrl? = nil) async -> MigrationF
}
}
// Spec: spec/services/files.md#receiveFile
func receiveFile(user: any UserLike, fileId: Int64, userApprovedRelays: Bool = false, auto: Bool = false) async {
await receiveFiles(
user: user,
@@ -1566,6 +1586,7 @@ func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool
}
}
// Spec: spec/services/files.md#cancelFile
func cancelFile(user: User, fileId: Int64) async {
if let chatItem = await apiCancelFile(fileId: fileId) {
await chatItemSimpleUpdate(user, chatItem)
@@ -1588,12 +1609,14 @@ func setLocalDeviceName(_ displayName: String) throws {
try sendCommandOkRespSync(.setLocalDeviceName(displayName: displayName))
}
// Spec: spec/architecture.md#connectRemoteCtrl
func connectRemoteCtrl(desktopAddress: String) async throws -> (RemoteCtrlInfo?, CtrlAppInfo, String) {
let r: ChatResponse2 = try await chatSendCmd(.connectRemoteCtrl(xrcpInvitation: desktopAddress))
if case let .remoteCtrlConnecting(rc_, ctrlAppInfo, v) = r { return (rc_, ctrlAppInfo, v) }
throw r.unexpected
}
// Spec: spec/architecture.md#findKnownRemoteCtrl
func findKnownRemoteCtrl() async throws {
try await sendCommandOkResp(.findKnownRemoteCtrl)
}
@@ -2071,6 +2094,7 @@ private func chatInitialized(start: Bool, refreshInvitations: Bool) throws {
}
}
// Spec: spec/architecture.md#startChat
func startChat(refreshInvitations: Bool = true, onboarding: Bool = false) throws {
logger.debug("startChat")
let m = ChatModel.shared
@@ -2192,6 +2216,7 @@ private func getUserChatDataAsync(keepingChatId: String?) async throws {
}
}
// Spec: spec/architecture.md#ChatReceiver
class ChatReceiver {
private var receiveLoop: Task<Void, Never>?
private var receiveMessages = true
@@ -2237,6 +2262,7 @@ class ChatReceiver {
}
}
// Spec: spec/api.md#processReceivedMsg
func processReceivedMsg(_ res: ChatEvent) async {
let m = ChatModel.shared
logger.debug("processReceivedMsg: \(res.responseType)")
+3
View File
@@ -4,6 +4,7 @@
//
// Created by Evgeny Poberezkin on 17/01/2022.
//
// Spec: spec/architecture.md
import SwiftUI
import OSLog
@@ -12,6 +13,7 @@ import SimpleXChat
let logger = Logger()
@main
// Spec: spec/architecture.md#SimpleXApp
struct SimpleXApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var chatModel = ChatModel.shared
@@ -60,6 +62,7 @@ struct SimpleXApp: App {
}
}
}
// Spec: spec/architecture.md#scenePhaseHandling
.onChange(of: scenePhase) { phase in
logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))")
AppSheetState.shared.scenePhaseActive = phase == .active
+3
View File
@@ -10,6 +10,7 @@ import Foundation
import SwiftUI
import SimpleXChat
// Spec: spec/services/theme.md#CurrentColors
var CurrentColors: ThemeManager.ActiveTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
var MenuTextColor: Color { if isInDarkTheme() { AppTheme.shared.colors.onBackground.opacity(0.8) } else { Color.black } }
@@ -17,6 +18,7 @@ var NoteFolderIconColor: Color { AppTheme.shared.appColors.primaryVariant2 }
func isInDarkTheme() -> Bool { !CurrentColors.colors.isLight }
// Spec: spec/services/theme.md#AppTheme
class AppTheme: ObservableObject, Equatable {
static let shared = AppTheme(name: CurrentColors.name, base: CurrentColors.base, colors: CurrentColors.colors, appColors: CurrentColors.appColors, wallpaper: CurrentColors.wallpaper)
@@ -89,6 +91,7 @@ struct ThemedBackground: ViewModifier {
}
}
// Spec: spec/services/theme.md#systemInDarkThemeCurrently
var systemInDarkThemeCurrently: Bool {
return UITraitCollection.current.userInterfaceStyle == .dark
}
+13
View File
@@ -5,12 +5,15 @@
// Created by Avently on 03.06.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
// Spec: spec/services/theme.md
import Foundation
import SwiftUI
import SimpleXChat
// Spec: spec/services/theme.md#ThemeManager
class ThemeManager {
// Spec: spec/services/theme.md#ActiveTheme
struct ActiveTheme: Equatable {
let name: String
let base: DefaultTheme
@@ -41,6 +44,7 @@ class ThemeManager {
}
}
// Spec: spec/services/theme.md#defaultActiveTheme
static func defaultActiveTheme(_ appSettingsTheme: [ThemeOverrides]) -> ThemeOverrides? {
let nonSystemThemeName = nonSystemThemeName()
let defaultThemeId = currentThemeIdsDefault.get()[nonSystemThemeName]
@@ -56,6 +60,7 @@ class ThemeManager {
return ThemeModeOverride(mode: CurrentColors.base.mode, colors: defaultTheme?.colors ?? ThemeColors(), wallpaper: defaultTheme?.wallpaper ?? ThemeWallpaper.from(PresetWallpaper.school.toType(CurrentColors.base), nil, nil))
}
// Spec: spec/services/theme.md#currentColors
static func currentColors(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?, _ appSettingsTheme: [ThemeOverrides]) -> ActiveTheme {
let themeName = currentThemeDefault.get()
let nonSystemThemeName = nonSystemThemeName()
@@ -96,6 +101,7 @@ class ThemeManager {
)
}
// Spec: spec/services/theme.md#currentThemeOverridesForExport
static func currentThemeOverridesForExport(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?) -> ThemeOverrides {
let current = currentColors(themeOverridesForType, perChatTheme, perUserTheme, themeOverridesDefault.get())
let wType = current.wallpaper.type
@@ -114,6 +120,7 @@ class ThemeManager {
)
}
// Spec: spec/services/theme.md#applyTheme
static func applyTheme(_ theme: String) {
currentThemeDefault.set(theme)
CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get())
@@ -125,6 +132,7 @@ class ThemeManager {
// applyNavigationBarColors(CurrentColors.toAppTheme())
}
// Spec: spec/services/theme.md#adjustWindowStyle
static func adjustWindowStyle() {
let style = switch currentThemeDefault.get() {
case DefaultTheme.LIGHT.themeName: UIUserInterfaceStyle.light
@@ -161,6 +169,7 @@ class ThemeManager {
AppTheme.shared.updateFromCurrentColors()
}
// Spec: spec/services/theme.md#saveAndApplyThemeColor
static func saveAndApplyThemeColor(_ baseTheme: DefaultTheme, _ name: ThemeColor, _ color: Color? = nil, _ pref: CodableDefault<[ThemeOverrides]>? = nil) {
let nonSystemThemeName = baseTheme.themeName
let pref = pref ?? themeOverridesDefault
@@ -178,6 +187,7 @@ class ThemeManager {
pref.wrappedValue = pref.wrappedValue.withUpdatedColor(name, color?.toReadableHex())
}
// Spec: spec/services/theme.md#saveAndApplyWallpaper
static func saveAndApplyWallpaper(_ baseTheme: DefaultTheme, _ type: WallpaperType?, _ pref: CodableDefault<[ThemeOverrides]>?) {
let nonSystemThemeName = baseTheme.themeName
let pref = pref ?? themeOverridesDefault
@@ -253,6 +263,7 @@ class ThemeManager {
pref.wrappedValue = prevValue
}
// Spec: spec/services/theme.md#saveAndApplyThemeOverrides
static func saveAndApplyThemeOverrides(_ theme: ThemeOverrides, _ pref: CodableDefault<[ThemeOverrides]>? = nil) {
let wallpaper = theme.wallpaper?.importFromString()
let nonSystemThemeName = theme.base.themeName
@@ -273,6 +284,7 @@ class ThemeManager {
applyTheme(nonSystemThemeName)
}
// Spec: spec/services/theme.md#resetAllThemeColors
static func resetAllThemeColors(_ pref: CodableDefault<[ThemeOverrides]>? = nil) {
let nonSystemThemeName = nonSystemThemeName()
let pref: CodableDefault<[ThemeOverrides]> = pref ?? themeOverridesDefault
@@ -295,6 +307,7 @@ class ThemeManager {
pref.wrappedValue = prevValue
}
// Spec: spec/services/theme.md#removeTheme
static func removeTheme(_ themeId: String?) {
var themes = themeOverridesDefault.get().map { $0 }
themes.removeAll(where: { $0.themeId == themeId })
@@ -5,12 +5,14 @@
// Created by Evgeny on 05/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/services/calls.md
import SwiftUI
import WebKit
import SimpleXChat
import AVFoundation
// Spec: spec/services/calls.md#ActiveCallView
struct ActiveCallView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
@@ -282,6 +284,7 @@ struct ActiveCallView: View {
}
}
// Spec: spec/services/calls.md#ActiveCallOverlay
struct ActiveCallOverlay: View {
@EnvironmentObject var chatModel: ChatModel
@ObservedObject var call: Call
@@ -350,6 +353,7 @@ struct ActiveCallOverlay: View {
}
}
// Spec: spec/services/calls.md#audioCallInfoView
private func audioCallInfoView(_ call: Call) -> some View {
VStack {
Text(call.contact.chatViewName)
@@ -399,6 +403,7 @@ struct ActiveCallOverlay: View {
}
}
// Spec: spec/services/calls.md#endCallButton
private func endCallButton() -> some View {
let cc = CallController.shared
return callButton("phone.down.fill", .red, padding: 10) {
@@ -5,6 +5,7 @@
// Created by Evgeny on 21/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/services/calls.md
import Foundation
import CallKit
@@ -14,6 +15,7 @@ import AVFoundation
import SimpleXChat
import WebRTC
// Spec: spec/services/calls.md#CallController
class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, ObservableObject {
static let shared = CallController()
static let isInChina = SKStorefront().countryCode == "CHN"
@@ -49,6 +51,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
logger.debug("CallController.providerDidReset")
}
// Spec: spec/services/calls.md#CXStartCallAction
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
logger.debug("CallController.provider CXStartCallAction")
if callManager.startOutgoingCall(callUUID: action.callUUID.uuidString.lowercased()) {
@@ -59,6 +62,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
}
}
// Spec: spec/services/calls.md#CXAnswerCallAction
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
logger.debug("CallController.provider CXAnswerCallAction")
Task {
@@ -88,6 +92,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
}
}
// Spec: spec/services/calls.md#CXEndCallAction
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
logger.debug("CallController.provider CXEndCallAction")
// Should be nil here if connection was in connected state
@@ -103,6 +108,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
}
}
// Spec: spec/services/calls.md#CXSetMutedCallAction
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
if callManager.enableMedia(source: .mic, enable: !action.isMuted, callUUID: action.callUUID.uuidString.lowercased()) {
action.fulfill()
@@ -192,6 +198,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
logger.debug("CallController: didUpdate push credentials for type \(type.rawValue)")
}
// Spec: spec/services/calls.md#pushRegistryDidReceive
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
logger.debug("CallController: did receive push with type \(type.rawValue)")
if type != .voIP {
@@ -276,6 +283,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
reportExpiredCall(update: update, completion)
}
// Spec: spec/services/calls.md#reportNewIncomingCall
func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) {
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callUUID))")
if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) {
@@ -316,6 +324,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
}
}
// Spec: spec/services/calls.md#reportOutgoingCall
func reportOutgoingCall(call: Call, connectedAt dateConnected: Date?) {
logger.debug("CallController: reporting outgoing call connected")
if CallController.useCallKit(), let callUUID = call.callUUID, let uuid = UUID(uuidString: callUUID) {
@@ -422,6 +431,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
provider.configuration = conf
}
// Spec: spec/services/calls.md#hasActiveCalls
func hasActiveCalls() -> Bool {
controller.callObserver.calls.count > 0
}
@@ -2,12 +2,14 @@
// Created by Avently on 09.02.2023.
// Copyright (c) 2023 SimpleX Chat. All rights reserved.
//
// Spec: spec/services/calls.md
import WebRTC
import LZString
import SwiftUI
import SimpleXChat
// Spec: spec/services/calls.md#WebRTCClient
final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDelegate, RTCFrameDecryptorDelegate {
private static let factory: RTCPeerConnectionFactory = {
RTCInitializeSSL()
@@ -87,6 +89,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
WebRTC.RTCIceServer(urlStrings: ["turns:turn.simplex.im:443?transport=tcp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj"),
]
// Spec: spec/services/calls.md#initializeCall
func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call {
let connection = createPeerConnection(iceServers ?? getWebRTCIceServers() ?? defaultIceServers, relay)
connection.delegate = self
@@ -132,6 +135,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
)
}
// Spec: spec/services/calls.md#createPeerConnection
func createPeerConnection(_ iceServers: [WebRTC.RTCIceServer], _ relay: Bool?) -> RTCPeerConnection {
let constraints = RTCMediaConstraints(mandatoryConstraints: nil,
optionalConstraints: ["DtlsSrtpKeyAgreement": kRTCMediaConstraintsValueTrue])
@@ -157,6 +161,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
return config
}
// Spec: spec/services/calls.md#addIceCandidates
func addIceCandidates(_ connection: RTCPeerConnection, _ remoteIceCandidates: [RTCIceCandidate]) {
remoteIceCandidates.forEach { candidate in
connection.add(candidate.toWebRTCCandidate()) { error in
@@ -167,6 +172,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
}
// Spec: spec/services/calls.md#sendCallCommand
func sendCallCommand(command: WCallCommand) async {
var resp: WCallResponse? = nil
let pc = activeCall?.connection
@@ -295,6 +301,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
}
// Spec: spec/services/calls.md#sendIceCandidates
func sendIceCandidates(_ candidates: [RTCIceCandidate]) async {
await self.sendCallResponse(.init(
corrId: nil,
@@ -353,6 +360,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
}
// Spec: spec/services/calls.md#enableMedia
@MainActor
func enableMedia(_ source: CallMediaSource, _ enable: Bool) {
logger.debug("WebRTCClient: enabling media \(source.rawValue) \(enable)")
@@ -411,6 +419,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
localRendererAspectRatio.wrappedValue = size.width / size.height
}
// Spec: spec/services/calls.md#setupLocalTracks
func setupLocalTracks(_ incomingCall: Bool, _ call: Call) {
let pc = call.connection
let transceivers = call.connection.transceivers
@@ -490,6 +499,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
// Should be called after local description set
// Spec: spec/services/calls.md#setupEncryptionForLocalTracks
func setupEncryptionForLocalTracks(_ call: Call) {
if let encryptor = call.frameEncryptor {
call.connection.senders.forEach { $0.setRtcFrameEncryptor(encryptor) }
@@ -567,6 +577,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
}
// Spec: spec/services/calls.md#startCaptureLocalVideo
func startCaptureLocalVideo(_ device: AVCaptureDevice.Position?, _ capturer: RTCVideoCapturer?) {
#if targetEnvironment(simulator)
guard
@@ -630,6 +641,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
return (localCamera, localVideoTrack)
}
// Spec: spec/services/calls.md#endCall
func endCall() {
if #available(iOS 16.0, *) {
_endCall()
@@ -5,6 +5,7 @@
// Created by Evgeny Poberezkin on 05/02/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
@preconcurrency import SimpleXChat
@@ -88,6 +89,7 @@ enum SendReceipts: Identifiable, Hashable {
}
}
// Spec: spec/client/chat-view.md#ChatInfoView
struct ChatInfoView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@@ -2,10 +2,12 @@
// Created by Avently on 19.12.2022.
// Copyright (c) 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import UIKit
import SwiftUI
// Spec: spec/client/chat-view.md#AnimatedImageView
class AnimatedImageView: UIView {
var image: UIImage? = nil
var imageView: UIImageView? = nil
@@ -5,6 +5,7 @@
// Created by Evgeny on 20/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
@@ -5,10 +5,12 @@
// Created by Evgeny on 21/11/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#CIChatFeatureView
struct CIChatFeatureView: View {
@EnvironmentObject var m: ChatModel
@Environment(\.revealed) var revealed: Bool
@@ -5,10 +5,12 @@
// Created by JRoberts on 20.07.2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#CIEventView
struct CIEventView: View {
var eventText: Text
@@ -5,10 +5,12 @@
// Created by Evgeny on 21/12/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#CIFeaturePreferenceView
struct CIFeaturePreferenceView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
@@ -5,10 +5,12 @@
// Created by JRoberts on 28/04/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#CIFileView
struct CIFileView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@@ -5,10 +5,12 @@
// Created by JRoberts on 15.07.2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#CIGroupInvitationView
struct CIGroupInvitationView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@@ -5,10 +5,12 @@
// Created by JRoberts on 12/04/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#CIImageView
struct CIImageView: View {
@EnvironmentObject var m: ChatModel
let chatItem: ChatItem
@@ -5,10 +5,12 @@
// Created by JRoberts on 29.12.2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#CIInvalidJSONView
struct CIInvalidJSONView: View {
@EnvironmentObject var theme: AppTheme
var json: Data?
@@ -5,10 +5,12 @@
// Created by Ian Davies on 07/04/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#CILinkView
struct CILinkView: View {
@EnvironmentObject var theme: AppTheme
let linkPreview: LinkPreview
@@ -5,10 +5,12 @@
// Created by spaced4ndy on 19.09.2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#CIMemberCreatedContactView
struct CIMemberCreatedContactView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@@ -5,10 +5,12 @@
// Created by Evgeny Poberezkin on 11/02/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#CIMetaView
struct CIMetaView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
@@ -5,12 +5,14 @@
// Created by Evgeny on 15/04/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
let decryptErrorReason: LocalizedStringKey = "It can happen when you or your connection used the old database backup."
// Spec: spec/client/chat-view.md#CIRcvDecryptionError
struct CIRcvDecryptionError: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@@ -5,12 +5,14 @@
// Created by Avently on 30/03/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import AVKit
import SimpleXChat
import Combine
// Spec: spec/client/chat-view.md#CIVideoView
struct CIVideoView: View {
@EnvironmentObject var m: ChatModel
private let chatItem: ChatItem
@@ -5,10 +5,12 @@
// Created by JRoberts on 22.11.2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#CIVoiceView
struct CIVoiceView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
@@ -5,10 +5,12 @@
// Created by JRoberts on 04/02/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#DeletedItemView
struct DeletedItemView: View {
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
@@ -5,10 +5,12 @@
// Created by Evgeny Poberezkin on 04/02/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#EmojiItemView
struct EmojiItemView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
@@ -5,12 +5,14 @@
// Created by JRoberts on 22.11.2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#FramedCIVoiceView
struct FramedCIVoiceView: View {
@EnvironmentObject var theme: AppTheme
@ObservedObject var chat: Chat
@@ -5,10 +5,12 @@
// Created by Evgeny Poberezkin on 04/02/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#FramedItemView
struct FramedItemView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@@ -5,12 +5,14 @@
// Created by Evgeny on 08/10/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
import SwiftyGif
import AVKit
// Spec: spec/client/chat-view.md#FullScreenMediaView
struct FullScreenMediaView: View {
@EnvironmentObject var m: ChatModel
@State var chatItem: ChatItem
@@ -5,10 +5,12 @@
// Created by Evgeny on 28/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#IntegrityErrorItemView
struct IntegrityErrorItemView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var theme: AppTheme
@@ -5,10 +5,12 @@
// Created by JRoberts on 30.11.2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#MarkedDeletedItemView
struct MarkedDeletedItemView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@@ -5,6 +5,7 @@
// Created by Evgeny on 13/03/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
@@ -23,6 +24,7 @@ private func typing(_ theme: AppTheme, _ descr: UIFontDescriptor, _ ws: [UIFont.
return res
}
// Spec: spec/client/chat-view.md#MsgContentView
struct MsgContentView: View {
@ObservedObject var chat: Chat
@Environment(\.showTimestamp) var showTimestamp: Bool
@@ -320,6 +322,7 @@ func messageText(
var bold: UIFont?
var italic: UIFont?
var snippet: UIFont?
var small: UIFont?
var mention: UIFont?
var secretIdx: Int = 0
for ft in fts {
@@ -351,6 +354,10 @@ func messageText(
attrs[.backgroundColor] = secretColor
}
hasSecrets = true
case .small:
small = small ?? UIFont.preferredFont(forTextStyle: .footnote)
attrs[.font] = small
attrs[.foregroundColor] = UIColor.secondaryLabel
case let .colored(color):
if let c = color.uiColor {
attrs[.foregroundColor] = UIColor(c)
@@ -9,6 +9,7 @@
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-view.md#ChatItemInfoView
struct ChatItemInfoView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.dismiss) var dismiss
@@ -38,6 +38,7 @@ extension EnvironmentValues {
}
}
// Spec: spec/client/chat-view.md#ChatItemView
struct ChatItemView: View {
@ObservedObject var chat: Chat
@ObservedObject var im: ItemsModel
@@ -15,6 +15,7 @@ func apiLoadMessages(
_ chatId: ChatId,
_ im: ItemsModel,
_ pagination: ChatPagination,
_ contentTag: MsgContentTag? = nil,
_ search: String = "",
_ openAroundItemId: ChatItem.ID? = nil,
_ visibleItemIndexesNonReversed: @MainActor () -> ClosedRange<Int> = { 0 ... 0 }
@@ -22,7 +23,7 @@ func apiLoadMessages(
let chat: Chat
let navInfo: NavigationInfo
do {
(chat, navInfo) = try await apiGetChat(chatId: chatId, scope: im.groupScopeInfo?.toChatScope(), contentTag: im.contentTag, pagination: pagination, search: search)
(chat, navInfo) = try await apiGetChat(chatId: chatId, scope: im.groupScopeInfo?.toChatScope(), contentTag: contentTag ?? im.contentTag, pagination: pagination, search: search)
} catch let error {
logger.error("apiLoadMessages error: \(responseError(error))")
return
+178 -29
View File
@@ -5,6 +5,7 @@
// Created by Evgeny Poberezkin on 27/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
@@ -13,6 +14,7 @@ import Combine
private let memberImageSize: CGFloat = 34
// Spec: spec/client/chat-view.md#ChatView
struct ChatView: View {
@EnvironmentObject var chatModel: ChatModel
@StateObject private var connectProgressManager = ConnectProgressManager.shared
@@ -44,6 +46,8 @@ struct ChatView: View {
@State private var showSearch = false
@State private var searchText: String = ""
@FocusState private var searchFocussed
@State private var contentFilter: ContentFilter? = nil
@State private var availableContent: [ContentFilter] = [.images, .files, .links]
// opening GroupMemberInfoView on member icon
@State private var selectedMember: GMember? = nil
// opening GroupLinkView on link button (incognito)
@@ -68,6 +72,7 @@ struct ChatView: View {
let userSupportScopeInfo: GroupChatScopeInfo = .memberSupport(groupMember_: nil)
// Spec: spec/client/chat-view.md#body
var body: some View {
if #available(iOS 16.0, *) {
viewBody
@@ -528,16 +533,19 @@ struct ChatView: View {
case let .direct(contact):
HStack {
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser
if callsPrefEnabled {
if chatModel.activeCall == nil {
callButton(contact, .audio, imageName: "phone")
.disabled(!contact.ready || !contact.active)
} else if let call = chatModel.activeCall, call.contact.id == cInfo.id {
endCallButton(call)
}
if let call = chatModel.activeCall, call.contact.id == cInfo.id {
endCallButton(call)
} else {
contentFilterMenu(withLabel: false)
}
Menu {
if callsPrefEnabled && chatModel.activeCall == nil {
Button {
CallController.shared.startCall(contact, .audio)
} label: {
Label("Audio call", systemImage: "phone")
}
.disabled(!contact.ready || !contact.active)
Button {
CallController.shared.startCall(contact, .video)
} label: {
@@ -545,6 +553,9 @@ struct ChatView: View {
}
.disabled(!contact.ready || !contact.active)
}
if let call = chatModel.activeCall, call.contact.id == cInfo.id {
contentFilterMenu(withLabel: true)
}
searchButton()
ToggleNtfsButton(chat: chat)
.disabled(!contact.ready || !contact.active)
@@ -554,23 +565,24 @@ struct ChatView: View {
}
case let .group(groupInfo, _):
HStack {
if groupInfo.canAddMembers {
if (chat.chatInfo.incognito) {
groupLinkButton()
.appSheet(isPresented: $showGroupLinkSheet) {
GroupLinkView(
groupId: groupInfo.groupId,
groupLink: $groupLink,
groupLinkMemberRole: $groupLinkMemberRole,
showTitle: true,
creatingGroup: false
)
}
} else {
addMembersButton()
}
}
contentFilterMenu(withLabel: false)
Menu {
if groupInfo.canAddMembers {
if (chat.chatInfo.incognito) {
groupLinkButton()
.appSheet(isPresented: $showGroupLinkSheet) {
GroupLinkView(
groupId: groupInfo.groupId,
groupLink: $groupLink,
groupLinkMemberRole: $groupLinkMemberRole,
showTitle: true,
creatingGroup: false
)
}
} else {
addMembersButton()
}
}
searchButton()
ToggleNtfsButton(chat: chat)
} label: {
@@ -578,7 +590,10 @@ struct ChatView: View {
}
}
case .local:
searchButton()
HStack {
contentFilterMenu(withLabel: false)
searchButton()
}
default:
EmptyView()
}
@@ -656,6 +671,7 @@ struct ChatView: View {
.frame(width: 220)
}
// Spec: spec/client/chat-view.md#initChatView
private func initChatView() {
let cInfo = chat.chatInfo
// This check prevents the call to apiContactInfo after the app is suspended, and the database is closed.
@@ -685,6 +701,7 @@ struct ChatView: View {
}
}
}
updateAvailableContent()
}
if chatModel.draftChatId == cInfo.id && !composeState.forwarding,
let draft = chatModel.draft {
@@ -698,6 +715,23 @@ struct ChatView: View {
floatingButtonModel.updateOnListChange(scrollView.listState)
}
private func updateAvailableContent() {
Task {
let content: [ContentFilter]
do {
let contentTags = Set(try await apiGetChatContentTypes(chatId: chat.chatInfo.id)).union(ContentFilter.alwaysShow)
content = ContentFilter.allCases.filter { contentTags.contains($0.contentTag) }
} catch let error {
logger.error("apiGetChatContentTypes error: \(responseError(error))")
content = ContentFilter.allCases
}
await MainActor.run {
availableContent = content
}
}
}
// Spec: spec/client/chat-view.md#scrollToItem
private func scrollToItem(_ itemId: ChatItem.ID) {
Task {
do {
@@ -731,11 +765,16 @@ struct ChatView: View {
}
}
// Spec: spec/client/chat-view.md#searchToolbar
private func searchToolbar() -> some View {
HStack(spacing: 12) {
let placeholder: LocalizedStringKey = contentFilter?.searchPlaceholder ?? "Search"
return HStack(spacing: 12) {
HStack(spacing: 4) {
Image(systemName: "magnifyingglass")
TextField("Search", text: $searchText)
if let contentFilter {
Image(systemName: contentFilter.icon)
}
TextField(placeholder, text: $searchText)
.focused($searchFocussed)
.foregroundColor(theme.colors.onBackground)
.frame(maxWidth: .infinity)
@@ -764,6 +803,7 @@ struct ChatView: View {
ci.content.msgContent?.isVoice == true && ci.content.text.count == 0 && ci.quotedItem == nil && ci.meta.itemForwarded == nil
}
// Spec: spec/client/chat-view.md#filtered
private func filtered(_ reversedChatItems: Array<ChatItem>) -> Array<ChatItem> {
reversedChatItems
.enumerated()
@@ -777,6 +817,7 @@ struct ChatView: View {
.map { $0.element }
}
// Spec: spec/client/chat-view.md#chatItemsList
private func chatItemsList() -> some View {
let cInfo = chat.chatInfo
return GeometryReader { g in
@@ -1050,9 +1091,10 @@ struct ChatView: View {
}
}
// Spec: spec/client/chat-view.md#searchTextChanged
private func searchTextChanged(_ s: String) {
Task {
await loadChat(chat: chat, im: im, search: s)
await loadChat(chat: chat, im: im, contentTag: contentFilter?.contentTag, search: s)
mergedItems.boxedValue = MergedItems.create(im, revealedItems)
await MainActor.run {
scrollView.updateItems(mergedItems.boxedValue.items)
@@ -1227,6 +1269,7 @@ struct ChatView: View {
}
}
// Spec: spec/client/chat-view.md#callButton
private func callButton(_ contact: Contact, _ media: CallMediaType, imageName: String) -> some View {
Button {
CallController.shared.startCall(contact, media)
@@ -1255,16 +1298,52 @@ struct ChatView: View {
}
}
private func contentFilterMenu(withLabel: Bool) -> some View {
Menu {
ForEach(availableContent, id: \.self) { type in
Button {
setContentFilter(type)
} label: {
Label(type.label, systemImage: contentFilter == type ? type.iconFilled : type.icon)
}
}
if contentFilter != nil {
Button {
closeSearch()
} label: {
Label("All messages", systemImage: "bubble.left.and.text.bubble.right")
}
}
} label: {
let icon = contentFilter == nil ? "photo.on.rectangle" : "photo.on.rectangle.fill"
if withLabel {
Label("Filter", systemImage: icon)
} else {
Image(systemName: icon)
}
}
}
private func focusSearch() {
showSearch = true
searchFocussed = true
searchText = ""
}
private func setContentFilter(_ type: ContentFilter) {
if (contentFilter == type) { return }
contentFilter = type
showSearch = true
searchText = ""
searchTextChanged("")
}
private func closeSearch() {
showSearch = false
searchText = ""
searchFocussed = false
contentFilter = nil
updateAvailableContent()
}
private func closeKeyboardAndRun(_ action: @escaping () -> Void) {
@@ -1285,7 +1364,7 @@ struct ChatView: View {
Task { await chatModel.loadGroupMembers(gInfo) { showAddMembersSheet = true } }
}
} label: {
Image(systemName: "person.crop.circle.badge.plus")
Label("Invite member", systemImage: "person.crop.circle.badge.plus")
}
}
@@ -1305,7 +1384,7 @@ struct ChatView: View {
}
}
} label: {
Image(systemName: "link.badge.plus")
Label("Group link", systemImage: "link.badge.plus")
}
}
@@ -1328,6 +1407,7 @@ struct ChatView: View {
))
}
// Spec: spec/client/chat-view.md#deletedSelectedMessages
private func deletedSelectedMessages() async {
await MainActor.run {
withAnimation {
@@ -1336,6 +1416,7 @@ struct ChatView: View {
}
}
// Spec: spec/client/chat-view.md#forwardSelectedMessages
private func forwardSelectedMessages() {
Task {
do {
@@ -1446,6 +1527,7 @@ struct ChatView: View {
}
}
// Spec: spec/client/chat-view.md#loadChatItems
private func loadChatItems(_ chat: Chat, _ pagination: ChatPagination) async -> Bool {
if loadingMoreItems { return false }
await MainActor.run {
@@ -1473,6 +1555,7 @@ struct ChatView: View {
chat.chatInfo.id,
im,
pagination,
contentFilter?.contentTag,
searchText,
nil,
{ visibleItemIndexesNonReversed(im, scrollView.listState, mergedItems.boxedValue) }
@@ -1485,6 +1568,7 @@ struct ChatView: View {
VoiceItemState.chatView = [:]
}
// Spec: spec/client/chat-view.md#onChatItemsUpdated
func onChatItemsUpdated() {
if !mergedItems.boxedValue.isActualState() {
//logger.debug("Items are not actual, waiting for the next update: \(String(describing: mergedItems.boxedValue.splits)) \(im.chatState.splits), \(mergedItems.boxedValue.indexInParentItems.count) vs \(im.reversedChatItems.count)")
@@ -1512,6 +1596,7 @@ struct ChatView: View {
)
}
// Spec: spec/client/chat-view.md#ChatItemWithMenu
private struct ChatItemWithMenu: View {
@ObservedObject var im: ItemsModel
@EnvironmentObject var m: ChatModel
@@ -2623,6 +2708,7 @@ struct ChatView: View {
}
}
// Spec: spec/client/chat-view.md#FloatingButtonModel
class FloatingButtonModel: ObservableObject {
@ObservedObject var im: ItemsModel
@@ -2705,6 +2791,7 @@ private func broadcastDeleteButtonText(_ chat: Chat) -> LocalizedStringKey {
chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone"
}
// Spec: spec/client/chat-view.md#deleteMessages
private func deleteMessages(_ chat: Chat, _ deletingItems: [Int64], _ mode: CIDeleteMode = .cidmInternal, moderate: Bool, _ onSuccess: @escaping () async -> Void = {}) {
let itemIds = deletingItems
if itemIds.count > 0 {
@@ -2808,6 +2895,7 @@ private func buildTheme() -> AppTheme {
}
}
// Spec: spec/client/chat-view.md#ReactionContextMenu
struct ReactionContextMenu: View {
@EnvironmentObject var m: ChatModel
let groupInfo: GroupInfo
@@ -2957,6 +3045,67 @@ func updateChatSettings(_ chat: Chat, chatSettings: ChatSettings) {
}
}
// Spec: spec/client/chat-view.md#ContentFilter
enum ContentFilter: CaseIterable {
case images
case videos
case voice
case files
case links
static let alwaysShow: Set<MsgContentTag> = [.image, .link]
var contentTag: MsgContentTag {
switch self {
case .images: .image
case .videos: .video
case .voice: .voice
case .files: .file
case .links: .link
}
}
var label: LocalizedStringKey {
switch self {
case .images: "Images"
case .videos: "Videos"
case .voice: "Voice messages"
case .files: "Files"
case .links: "Links"
}
}
var searchPlaceholder: LocalizedStringKey {
switch self {
case .images: "Search images"
case .videos: "Search videos"
case .voice: "Search voice messages"
case .files: "Search files"
case .links: "Search links"
}
}
var icon: String {
switch self {
case .images: "photo"
case .videos: "video"
case .voice: "mic"
case .files: "doc"
case .links: "link"
}
}
var iconFilled: String {
switch self {
case .images: "photo.fill"
case .videos: "video.fill"
case .voice: "mic.fill"
case .files: "doc.fill"
case .links: "link.circle.fill"
}
}
}
struct ChatView_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
@@ -1,3 +1,4 @@
// Spec: spec/client/compose.md
import SwiftUI
import SimpleXChat
@@ -6,6 +7,7 @@ import PhotosUI
let MAX_NUMBER_OF_MENTIONS = 3
// Spec: spec/client/compose.md#ComposePreview
enum ComposePreview {
case noPreview
case linkPreview(linkPreview: LinkPreview?)
@@ -14,6 +16,7 @@ enum ComposePreview {
case filePreview(fileName: String, file: URL)
}
// Spec: spec/client/compose.md#ComposeContextItem
enum ComposeContextItem: Equatable {
case noContextItem
case quotedItem(chatItem: ChatItem)
@@ -22,12 +25,14 @@ enum ComposeContextItem: Equatable {
case reportedItem(chatItem: ChatItem, reason: ReportReason)
}
// Spec: spec/client/compose.md#VoiceMessageRecordingState
enum VoiceMessageRecordingState {
case noRecording
case recording
case finished
}
// Spec: spec/client/compose.md#LiveMessage
struct LiveMessage {
var chatItem: ChatItem
var typedMsg: String
@@ -36,6 +41,7 @@ struct LiveMessage {
typealias MentionedMembers = [String: CIMention]
// Spec: spec/client/compose.md#ComposeState
struct ComposeState {
var message: String
var parsedMessage: [FormattedText]
@@ -256,6 +262,7 @@ struct ComposeState {
}
}
// Spec: spec/client/compose.md#chatItemPreview
func chatItemPreview(chatItem: ChatItem) -> ComposePreview {
switch chatItem.content.msgContent {
case .text:
@@ -276,6 +283,7 @@ func chatItemPreview(chatItem: ChatItem) -> ComposePreview {
}
}
// Spec: spec/client/compose.md#UploadContent
enum UploadContent: Equatable {
case simpleImage(image: UIImage)
case animatedImage(image: UIImage)
@@ -317,6 +325,7 @@ enum UploadContent: Equatable {
}
}
// Spec: spec/client/compose.md#ComposeView
struct ComposeView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@@ -356,6 +365,7 @@ struct ComposeView: View {
@AppStorage(GROUP_DEFAULT_PRIVACY_SANITIZE_LINKS, store: groupDefaults) private var privacySanitizeLinks = false
@State private var updatingCompose = false
// Spec: spec/client/compose.md#body
var body: some View {
VStack(spacing: 0) {
Divider()
@@ -679,6 +689,7 @@ struct ComposeView: View {
.padding(.horizontal, 12)
}
// Spec: spec/client/compose.md#sendMessageView
private func sendMessageView(_ disableSendButton: Bool, placeholder: String? = nil, sendToConnect: (() -> Void)? = nil) -> some View {
ZStack(alignment: .leading) {
SendMessageView(
@@ -878,6 +889,7 @@ struct ComposeView: View {
}
}
// Spec: spec/client/compose.md#addMediaContent
private func addMediaContent(_ content: UploadContent) async {
if let img = await resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
var newMedia: [(String, UploadContent?)] = []
@@ -906,6 +918,7 @@ struct ComposeView: View {
getMaxFileSize(.xftp)
}
// Spec: spec/client/compose.md#sendLiveMessage
private func sendLiveMessage() async {
let typedMsg = composeState.message
let lm = composeState.liveMessage
@@ -923,6 +936,7 @@ struct ComposeView: View {
}
}
// Spec: spec/client/compose.md#updateLiveMessage
private func updateLiveMessage() async {
let typedMsg = composeState.message
if let liveMessage = composeState.liveMessage {
@@ -941,6 +955,7 @@ struct ComposeView: View {
}
}
// Spec: spec/client/compose.md#liveMessageToSend
private func liveMessageToSend(_ lm: LiveMessage, _ t: String) -> String? {
let s = t != lm.typedMsg ? truncateToWords(t) : t
return s != lm.sentMsg && (lm.sentMsg != nil || !s.isEmpty) ? s : nil
@@ -1087,6 +1102,7 @@ struct ComposeView: View {
}
}
// Spec: spec/client/compose.md#sendMessage
private func sendMessage(ttl: Int?) {
logger.debug("ChatView sendMessage")
Task {
@@ -1095,6 +1111,7 @@ struct ComposeView: View {
}
}
// Spec: spec/client/compose.md#sendMessageAsync
private func sendMessageAsync(_ text: String?, live: Bool, ttl: Int?) async -> ChatItem? {
var sent: ChatItem?
let msgText = text ?? composeState.message
@@ -1361,6 +1378,7 @@ struct ComposeView: View {
await MainActor.run { composeState.inProgress = true }
}
// Spec: spec/client/compose.md#startVoiceMessageRecording
private func startVoiceMessageRecording() async {
startingRecording = true
let fileName = generateNewFileName("voice", "m4a")
@@ -1401,6 +1419,7 @@ struct ComposeView: View {
}
}
// Spec: spec/client/compose.md#finishVoiceMessageRecording
private func finishVoiceMessageRecording() {
audioRecorder?.stop()
audioRecorder = nil
@@ -1411,6 +1430,7 @@ struct ComposeView: View {
}
}
// Spec: spec/client/compose.md#allowVoiceMessagesToContact
private func allowVoiceMessagesToContact() {
if case let .direct(contact) = chat.chatInfo {
allowFeatureToContact(contact, .voice)
@@ -1436,12 +1456,14 @@ struct ComposeView: View {
}
}
// Spec: spec/client/compose.md#cancelVoiceMessageRecording
private func cancelVoiceMessageRecording(_ fileName: String) {
stopPlayback.toggle()
audioRecorder?.stop()
removeFile(fileName)
}
// Spec: spec/client/compose.md#clearState
private func clearState(live: Bool = false) {
if live {
composeState.inProgress = false
@@ -1455,11 +1477,13 @@ struct ComposeView: View {
startingRecording = false
}
// Spec: spec/client/compose.md#saveCurrentDraft
private func saveCurrentDraft() {
chatModel.draft = composeState
chatModel.draftChatId = chat.id
}
// Spec: spec/client/compose.md#clearCurrentDraft
private func clearCurrentDraft() {
if chatModel.draftChatId == chat.id {
chatModel.draft = nil
@@ -1467,6 +1491,7 @@ struct ComposeView: View {
}
}
// Spec: spec/client/compose.md#showLinkPreview
private func showLinkPreview(_ parsedMsg: [FormattedText]?) {
prevLinkUrl = linkUrl
(linkUrl, hasSimplexLink) = getMessageLinks(parsedMsg)
@@ -1486,6 +1511,7 @@ struct ComposeView: View {
}
}
// Spec: spec/client/compose.md#getMessageLinks
private func getMessageLinks(_ parsedMsg: [FormattedText]?) -> (url: String?, hasSimplexLink: Bool) {
guard let parsedMsg else { return (nil, false) }
let simplexLink = parsedMsgHasSimplexLink(parsedMsg)
@@ -1512,6 +1538,7 @@ struct ComposeView: View {
composeState = composeState.copy(preview: .noPreview)
}
// Spec: spec/client/compose.md#loadLinkPreview
private func loadLinkPreview(_ urlStr: String) {
if pendingLinkUrl == urlStr, let url = URL(string: urlStr) {
composeState = composeState.copy(preview: .linkPreview(linkPreview: nil))
@@ -11,6 +11,7 @@ import SimpleXChat
private let liveMsgInterval: UInt64 = 3000_000000
// Spec: spec/client/compose.md#SendMessageView
struct SendMessageView: View {
var placeholder: String?
@Binding var composeState: ComposeState
@@ -5,6 +5,7 @@
// Created by JRoberts on 22.07.2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
@@ -5,12 +5,14 @@
// Created by JRoberts on 14.07.2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
let SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
// Spec: spec/client/chat-view.md#GroupChatInfoView
struct GroupChatInfoView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@@ -453,7 +455,9 @@ struct GroupChatInfoView: View {
}
private func memberConnStatus(_ member: GroupMember) -> LocalizedStringKey {
if member.activeConn?.connDisabled ?? false {
if case .failed = member.activeConn?.connStatus {
return "failed"
} else if member.activeConn?.connDisabled ?? false {
return "disabled"
} else if member.activeConn?.connInactive ?? false {
return "inactive"
@@ -5,6 +5,7 @@
// Created by JRoberts on 15.10.2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by JRoberts on 25.07.2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-view.md
import SwiftUI
import SimpleXChat
@@ -188,6 +189,12 @@ struct GroupMemberInfoView: View {
}
}
if let connFailedErr = member.activeConn?.connFailedErr {
Section {
infoRow("Connection failed", connFailedErr)
}
}
if groupInfo.membership.memberRole >= .moderator {
adminDestructiveSection(member)
} else {
@@ -196,7 +196,9 @@ struct MemberSupportView: View {
}
private func memberStatus(_ member: GroupMember) -> LocalizedStringKey {
if member.activeConn?.connDisabled ?? false {
if case .failed = member.activeConn?.connStatus {
return "failed"
} else if member.activeConn?.connDisabled ?? false {
return "disabled"
} else if member.activeConn?.connInactive ?? false {
return "inactive"
@@ -40,6 +40,7 @@ func dynamicSize(_ font: DynamicTypeSize) -> DynamicSizes {
dynamicSizes[font] ?? defaultDynamicSizes
}
// Spec: spec/client/chat-list.md#ChatListNavLink
struct ChatListNavLink: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@@ -90,6 +91,7 @@ struct ChatListNavLink: View {
.actionSheet(item: $actionSheet) { $0.actionSheet }
}
// Spec: spec/client/chat-list.md#contactNavLink
private func contactNavLink(_ contact: Contact) -> some View {
Group {
if contact.isContactCard {
@@ -211,6 +213,7 @@ struct ChatListNavLink: View {
}
}
// Spec: spec/client/chat-list.md#groupNavLink
@ViewBuilder private func groupNavLink(_ groupInfo: GroupInfo) -> some View {
switch (groupInfo.membership.memberStatus) {
case .memInvited:
@@ -295,6 +298,7 @@ struct ChatListNavLink: View {
}
}
// Spec: spec/client/chat-list.md#noteFolderNavLink
private func noteFolderNavLink(_ noteFolder: NoteFolder) -> some View {
NavLinkPlain(
chatId: chat.chatInfo.id,
@@ -325,6 +329,7 @@ struct ChatListNavLink: View {
.tint(chat.chatInfo.incognito ? .indigo : theme.colors.primary)
}
// Spec: spec/client/chat-list.md#markReadButton
@ViewBuilder private func markReadButton() -> some View {
if chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat {
Button {
@@ -344,6 +349,7 @@ struct ChatListNavLink: View {
}
// Spec: spec/client/chat-list.md#toggleFavoriteButton
@ViewBuilder private func toggleFavoriteButton() -> some View {
if chat.chatInfo.chatSettings?.favorite == true {
Button {
@@ -362,6 +368,7 @@ struct ChatListNavLink: View {
}
}
// Spec: spec/client/chat-list.md#toggleNtfsButton
@ViewBuilder private func toggleNtfsButton(chat: Chat) -> some View {
if let nextMode = chat.chatInfo.nextNtfMode {
Button {
@@ -382,6 +389,7 @@ struct ChatListNavLink: View {
}
}
// Spec: spec/client/chat-list.md#clearChatButton
private func clearChatButton() -> some View {
Button {
AlertManager.shared.showAlert(clearChatAlert())
@@ -483,6 +491,7 @@ struct ChatListNavLink: View {
.tint(.red)
}
// Spec: spec/client/chat-list.md#contactRequestNavLink
private func contactRequestNavLink(_ contactRequest: UserContactRequest) -> some View {
ContactRequestView(contactRequest: contactRequest, chat: chat)
.frameCompat(height: dynamicRowHeight)
@@ -517,6 +526,7 @@ struct ChatListNavLink: View {
}
}
// Spec: spec/client/chat-list.md#contactConnectionNavLink
private func contactConnectionNavLink(_ contactConnection: PendingContactConnection) -> some View {
ContactConnectionView(chat: chat)
.frameCompat(height: dynamicRowHeight)
@@ -5,6 +5,7 @@
// Created by Evgeny Poberezkin on 27/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/chat-list.md
import SwiftUI
import SimpleXChat
@@ -31,6 +32,7 @@ enum UserPickerSheet: Identifiable {
}
}
// Spec: spec/client/chat-list.md#PresetTag
enum PresetTag: Int, Identifiable, CaseIterable, Equatable {
case groupReports = 0
case favorites = 1
@@ -46,6 +48,7 @@ enum PresetTag: Int, Identifiable, CaseIterable, Equatable {
}
}
// Spec: spec/client/chat-list.md#ActiveFilter
enum ActiveFilter: Identifiable, Equatable {
case presetTag(PresetTag)
case userTag(ChatTag)
@@ -135,6 +138,7 @@ struct UserPickerSheetView: View {
}
}
// Spec: spec/client/chat-list.md#ChatListView
struct ChatListView: View {
@EnvironmentObject var chatModel: ChatModel
@StateObject private var connectProgressManager = ConnectProgressManager.shared
@@ -160,6 +164,7 @@ struct ChatListView: View {
@AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
// Spec: spec/client/chat-list.md#body
var body: some View {
if #available(iOS 16.0, *) {
viewBody.scrollDismissesKeyboard(.immediately)
@@ -445,6 +450,7 @@ struct ChatListView: View {
}
// Spec: spec/client/chat-list.md#unreadBadge
private func unreadBadge(size: CGFloat = 18) -> some View {
Circle()
.frame(width: size, height: size)
@@ -464,11 +470,13 @@ struct ChatListView: View {
}
}
// Spec: spec/client/chat-list.md#stopAudioPlayer
func stopAudioPlayer() {
VoiceItemState.smallView.values.forEach { $0.audioPlayer?.stop() }
VoiceItemState.smallView = [:]
}
// Spec: spec/client/chat-list.md#filteredChats
private func filteredChats() -> [Chat] {
if let linkChatId = searchChatFilteredBySimplexLink {
return chatModel.chats.filter { $0.id == linkChatId }
@@ -511,6 +519,7 @@ struct ChatListView: View {
}
}
// Spec: spec/client/chat-list.md#searchString
func searchString() -> String {
searchShowingSimplexLink ? "" : searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
}
@@ -574,6 +583,7 @@ struct SubsStatusIndicator: View {
}
}
// Spec: spec/client/chat-list.md#ChatListSearchBar
struct ChatListSearchBar: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@@ -875,6 +885,7 @@ struct TagsView: View {
}
}
// Spec: spec/client/chat-list.md#setActiveFilter
private func setActiveFilter(filter: ActiveFilter) {
if filter != chatTagsModel.activeFilter {
chatTagsModel.activeFilter = filter
@@ -895,6 +906,7 @@ func chatStoppedIcon() -> some View {
}
}
// Spec: spec/client/chat-list.md#presetTagMatchesChat
func presetTagMatchesChat(_ tag: PresetTag, _ chatInfo: ChatInfo, _ chatStats: ChatStats) -> Bool {
switch tag {
case .groupReports:
@@ -9,6 +9,7 @@
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-list.md#ChatPreviewView
struct ChatPreviewView: View {
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@@ -16,6 +16,7 @@ struct TagEditorNavParams {
let tagId: Int64?
}
// Spec: spec/client/chat-list.md#TagListView
struct TagListView: View {
var chat: Chat? = nil
@Environment(\.dismiss) var dismiss: DismissAction
@@ -6,6 +6,7 @@
import SwiftUI
import SimpleXChat
// Spec: spec/client/chat-list.md#UserPicker
struct UserPicker: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@@ -5,6 +5,7 @@
// Created by Evgeny on 04/09/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/database.md
import SwiftUI
import SimpleXChat
@@ -33,6 +34,7 @@ enum DatabaseEncryptionAlert: Identifiable {
}
}
// Spec: spec/database.md#DatabaseEncryptionView
struct DatabaseEncryptionView: View {
@EnvironmentObject private var m: ChatModel
@EnvironmentObject private var theme: AppTheme
@@ -5,6 +5,7 @@
// Created by Evgeny on 04/09/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/database.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by Evgeny on 19/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/database.md
import SwiftUI
import SimpleXChat
@@ -41,6 +42,7 @@ enum DatabaseAlert: Identifiable {
}
}
// Spec: spec/database.md#DatabaseView
struct DatabaseView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@@ -5,6 +5,7 @@
// Created by Evgeny on 20/06/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/database.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by Evgeny on 10/04/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
// Spec: spec/architecture.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by Evgeny on 10/04/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
// Spec: spec/architecture.md
import SwiftUI
@@ -5,6 +5,7 @@
// Created by Evgeny on 11/04/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
// Spec: spec/architecture.md
import SwiftUI
@@ -5,6 +5,7 @@
// Created by Evgeny on 10/04/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
// Spec: spec/architecture.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by Avently on 14.02.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
// Spec: spec/database.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by Avently on 23.02.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
// Spec: spec/database.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by spaced4ndy on 28.11.2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/navigation.md
import SwiftUI
import SimpleXChat
@@ -73,6 +74,7 @@ func showKeepInvitationAlert() {
ChatModel.shared.showingInvitation = nil
}
// Spec: spec/client/navigation.md#NewChatView
struct NewChatView: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@@ -1163,6 +1165,7 @@ private func showOpenKnownGroupAlert(
)
}
// Spec: spec/client/navigation.md#planAndConnect
func planAndConnect(
_ shortOrFullLink: String,
theme: AppTheme,
@@ -5,6 +5,7 @@
// Created by Evgeny Poberezkin on 30/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/navigation.md
import SwiftUI
import CoreImage.CIFilterBuiltins
@@ -5,6 +5,7 @@
// Created by Diogo Cunha on 13/11/2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/navigation.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by spaced4ndy on 31.10.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/navigation.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by Evgeny on 07/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/navigation.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by spaced4ndy on 28.04.2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/navigation.md
import SwiftUI
import Contacts
@@ -5,6 +5,7 @@
// Created by Evgeny on 08/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/navigation.md
import SwiftUI
@@ -5,9 +5,11 @@
// Created by Evgeny on 07/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/navigation.md
import SwiftUI
// Spec: spec/client/navigation.md#OnboardingView
struct OnboardingView: View {
var onboarding: OnboardingStage
@@ -40,6 +42,7 @@ func onboardingButtonPlaceholder() -> some View {
Spacer().frame(height: 40)
}
// Spec: spec/client/navigation.md#onboardingStage
enum OnboardingStage: String, Identifiable {
case step1_SimpleXInfo
case step2_CreateProfile // deprecated
@@ -5,6 +5,7 @@
// Created by Evgeny on 03/07/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/navigation.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by Evgeny on 07/05/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/navigation.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by Evgeny on 24/12/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/navigation.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by Evgeny on 03/08/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/services/theme.md
import SwiftUI
import SimpleXChat
@@ -21,6 +22,7 @@ let darkThemesWithoutBlackNames: [String] = [DefaultTheme.DARK.themeName, Defaul
let appSettingsURL = URL(string: UIApplication.openSettingsURLString)!
// Spec: spec/services/theme.md#AppearanceSettings
struct AppearanceSettings: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
@@ -313,6 +315,7 @@ struct AppearanceSettings: View {
}
}
// Spec: spec/services/theme.md#ToolbarMaterial
enum ToolbarMaterial: String, CaseIterable {
case bar
case ultraThin
@@ -596,6 +599,7 @@ struct CustomizeThemeView: View {
}
}
// Spec: spec/services/theme.md#ImportExportThemeSection
struct ImportExportThemeSection: View {
@EnvironmentObject var theme: AppTheme
@Binding var showFileImporter: Bool
@@ -632,6 +636,7 @@ struct ImportExportThemeSection: View {
}
}
// Spec: spec/services/theme.md#ThemeImporter
struct ThemeImporter: ViewModifier {
@Binding var isPresented: Bool
var save: (ThemeOverrides) -> Void
@@ -1141,6 +1146,7 @@ private func removeUserThemeModeOverrides(_ themeUserDestination: Binding<(Int64
wallpaperFilesToDelete.forEach(removeWallpaperFile)
}
// Spec: spec/services/theme.md#decodeYAML
private func decodeYAML<T: Decodable>(_ string: String) -> T? {
do {
return try YAMLDecoder().decode(T.self, from: string)
@@ -1150,6 +1156,7 @@ private func decodeYAML<T: Decodable>(_ string: String) -> T? {
}
}
// Spec: spec/services/theme.md#encodeThemeOverrides
private func encodeThemeOverrides(_ value: ThemeOverrides) throws -> String {
let encoder = YAMLEncoder()
encoder.options = YAMLEncoder.Options(sequenceStyle: .block, mappingStyle: .block, newLineScalarStyle: .doubleQuoted)
@@ -5,6 +5,7 @@
// Created by Evgeny on 02/08/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/architecture.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by Stanislav Dmitrenko on 26.11.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
// Spec: spec/architecture.md
import SwiftUI
import WebKit
@@ -5,6 +5,7 @@
// Created by Evgeny on 02/08/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/architecture.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by spaced4ndy on 13.11.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
// Spec: spec/architecture.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by spaced4ndy on 28.10.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
// Spec: spec/architecture.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by Evgeny on 15/11/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/architecture.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by Evgeny on 15/11/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/architecture.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by Evgeny on 19/11/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/architecture.md
import SwiftUI
import SimpleXChat
@@ -5,6 +5,7 @@
// Created by Evgeny Poberezkin on 31/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/navigation.md
import SwiftUI
import StoreKit
@@ -2,6 +2,7 @@
// Created by Avently on 17.01.2023.
// Copyright (c) 2023 SimpleX Chat. All rights reserved.
//
// Spec: spec/client/navigation.md
import SwiftUI
import SimpleXChat
@@ -792,6 +792,10 @@ swipe action</note>
<target>Всички членове на групата ще останат свързани.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages" xml:space="preserve">
<source>All messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve">
<source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source>
<target>Всички съобщения и файлове се изпращат с **криптиране от край до край**, с постквантова сигурност в директните съобщения.</target>
@@ -1142,6 +1146,10 @@ swipe action</note>
<target>Аудио и видео разговори</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio call" xml:space="preserve">
<source>Audio call</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio/video calls" xml:space="preserve">
<source>Audio/video calls</source>
<target>Аудио/видео разговори</target>
@@ -2552,6 +2560,14 @@ swipe action</note>
<target>Изтрий съобщението на члена?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete member messages" xml:space="preserve">
<source>Delete member messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete member messages?" xml:space="preserve">
<source>Delete member messages?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Delete message?" xml:space="preserve">
<source>Delete message?</source>
<target>Изтрий съобщението?</target>
@@ -2560,7 +2576,8 @@ swipe action</note>
<trans-unit id="Delete messages" xml:space="preserve">
<source>Delete messages</source>
<target>Изтрий съобщенията</target>
<note>alert button</note>
<note>alert action
alert button</note>
</trans-unit>
<trans-unit id="Delete messages after" xml:space="preserve">
<source>Delete messages after</source>
@@ -3741,6 +3758,10 @@ snd error text</note>
<target>Файловете и медията са забранени!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter" xml:space="preserve">
<source>Filter</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter unread and favorite chats." xml:space="preserve">
<source>Filter unread and favorite chats.</source>
<target>Филтрирайте непрочетените и любимите чатове.</target>
@@ -4194,6 +4215,10 @@ Error: %2$@</source>
<target>Изображението ще бъде получено, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Images" xml:space="preserve">
<source>Images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Immediately" xml:space="preserve">
<source>Immediately</source>
<target>Веднага</target>
@@ -4442,6 +4467,10 @@ More improvements are coming soon!</source>
<target>Покани приятели</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invite member" xml:space="preserve">
<source>Invite member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invite members" xml:space="preserve">
<source>Invite members</source>
<target>Покани членове</target>
@@ -4658,6 +4687,10 @@ This is your link for group %@!</source>
<target>Запомнени настолни устройства</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Links" xml:space="preserve">
<source>Links</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="List" xml:space="preserve">
<source>List</source>
<note>swipe action</note>
@@ -4773,6 +4806,10 @@ This is your link for group %@!</source>
<source>Member is deleted - can't accept request</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Member messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>Member messages will be deleted - this cannot be undone!</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member reports" xml:space="preserve">
<source>Member reports</source>
<note>chat feature</note>
@@ -4793,12 +4830,12 @@ This is your link for group %@!</source>
</trans-unit>
<trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve">
<source>Member will be removed from chat - this cannot be undone!</source>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve">
<source>Member will be removed from group - this cannot be undone!</source>
<target>Членът ще бъде премахнат от групата - това не може да бъде отменено!</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member will join the group, accept member?" xml:space="preserve">
<source>Member will join the group, accept member?</source>
@@ -6296,7 +6333,11 @@ swipe action</note>
<trans-unit id="Remove" xml:space="preserve">
<source>Remove</source>
<target>Премахване</target>
<note>No comment provided by engineer.</note>
<note>alert action</note>
</trans-unit>
<trans-unit id="Remove and delete messages" xml:space="preserve">
<source>Remove and delete messages</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
@@ -6318,7 +6359,7 @@ swipe action</note>
<trans-unit id="Remove member?" xml:space="preserve">
<source>Remove member?</source>
<target>Острани член?</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Remove passphrase from keychain?" xml:space="preserve">
<source>Remove passphrase from keychain?</source>
@@ -6703,11 +6744,31 @@ chat item action</note>
<target>Лентата за търсене приема линк за връзка.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search files" xml:space="preserve">
<source>Search files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search images" xml:space="preserve">
<source>Search images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search links" xml:space="preserve">
<source>Search links</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<target>Търсене или поставяне на SimpleX линк</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search videos" xml:space="preserve">
<source>Search videos</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search voice messages" xml:space="preserve">
<source>Search voice messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secondary" xml:space="preserve">
<source>Secondary</source>
<note>No comment provided by engineer.</note>
@@ -8432,6 +8493,10 @@ To connect, please ask your contact to create another connection link and check
<target>Видеото ще бъде получено, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Videos" xml:space="preserve">
<source>Videos</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Videos and files up to 1gb" xml:space="preserve">
<source>Videos and files up to 1gb</source>
<target>Видео и файлове до 1gb</target>
@@ -782,6 +782,10 @@ swipe action</note>
<target>Všichni členové skupiny zůstanou připojeni.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages" xml:space="preserve">
<source>All messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve">
<source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source>
<note>No comment provided by engineer.</note>
@@ -1117,6 +1121,10 @@ swipe action</note>
<target>Hlasové a video hovory</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio call" xml:space="preserve">
<source>Audio call</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio/video calls" xml:space="preserve">
<source>Audio/video calls</source>
<target>Audio/video hovory</target>
@@ -2438,6 +2446,14 @@ swipe action</note>
<target>Smazat zprávu člena?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete member messages" xml:space="preserve">
<source>Delete member messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete member messages?" xml:space="preserve">
<source>Delete member messages?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Delete message?" xml:space="preserve">
<source>Delete message?</source>
<target>Smazat zprávu?</target>
@@ -2446,7 +2462,8 @@ swipe action</note>
<trans-unit id="Delete messages" xml:space="preserve">
<source>Delete messages</source>
<target>Smazat zprávy</target>
<note>alert button</note>
<note>alert action
alert button</note>
</trans-unit>
<trans-unit id="Delete messages after" xml:space="preserve">
<source>Delete messages after</source>
@@ -3596,6 +3613,10 @@ snd error text</note>
<target>Soubory a média jsou zakázány!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter" xml:space="preserve">
<source>Filter</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter unread and favorite chats." xml:space="preserve">
<source>Filter unread and favorite chats.</source>
<target>Filtrovat nepřečtené a oblíbené chaty.</target>
@@ -4037,6 +4058,10 @@ Error: %2$@</source>
<target>Obrázek bude přijat, až bude váš kontakt online, vyčkejte prosím nebo se podívejte později!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Images" xml:space="preserve">
<source>Images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Immediately" xml:space="preserve">
<source>Immediately</source>
<target>Ihned</target>
@@ -4272,6 +4297,10 @@ More improvements are coming soon!</source>
<target>Pozvat přátele</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invite member" xml:space="preserve">
<source>Invite member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invite members" xml:space="preserve">
<source>Invite members</source>
<target>Pozvat členy</target>
@@ -4479,6 +4508,10 @@ This is your link for group %@!</source>
<source>Linked desktops</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Links" xml:space="preserve">
<source>Links</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="List" xml:space="preserve">
<source>List</source>
<note>swipe action</note>
@@ -4594,6 +4627,10 @@ This is your link for group %@!</source>
<source>Member is deleted - can't accept request</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Member messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>Member messages will be deleted - this cannot be undone!</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member reports" xml:space="preserve">
<source>Member reports</source>
<note>chat feature</note>
@@ -4614,12 +4651,12 @@ This is your link for group %@!</source>
</trans-unit>
<trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve">
<source>Member will be removed from chat - this cannot be undone!</source>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve">
<source>Member will be removed from group - this cannot be undone!</source>
<target>Člen bude odstraněn ze skupiny - toto nelze vzít zpět!</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member will join the group, accept member?" xml:space="preserve">
<source>Member will join the group, accept member?</source>
@@ -6074,7 +6111,11 @@ swipe action</note>
<trans-unit id="Remove" xml:space="preserve">
<source>Remove</source>
<target>Odstranit</target>
<note>No comment provided by engineer.</note>
<note>alert action</note>
</trans-unit>
<trans-unit id="Remove and delete messages" xml:space="preserve">
<source>Remove and delete messages</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
@@ -6096,7 +6137,7 @@ swipe action</note>
<trans-unit id="Remove member?" xml:space="preserve">
<source>Remove member?</source>
<target>Odebrat člena?</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Remove passphrase from keychain?" xml:space="preserve">
<source>Remove passphrase from keychain?</source>
@@ -6471,10 +6512,30 @@ chat item action</note>
<source>Search bar accepts invitation links.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search files" xml:space="preserve">
<source>Search files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search images" xml:space="preserve">
<source>Search images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search links" xml:space="preserve">
<source>Search links</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search videos" xml:space="preserve">
<source>Search videos</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search voice messages" xml:space="preserve">
<source>Search voice messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secondary" xml:space="preserve">
<source>Secondary</source>
<note>No comment provided by engineer.</note>
@@ -8156,6 +8217,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu
<target>Video obdržíte, až bude váš kontakt online, vyčkejte prosím nebo zkontrolujte později!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Videos" xml:space="preserve">
<source>Videos</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Videos and files up to 1gb" xml:space="preserve">
<source>Videos and files up to 1gb</source>
<target>Videa a soubory až do velikosti 1 gb</target>
@@ -792,6 +792,10 @@ swipe action</note>
<target>Alle Gruppenmitglieder bleiben verbunden.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages" xml:space="preserve">
<source>All messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve">
<source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source>
<target>Alle Nachrichten und Dateien werden **Ende-zu-Ende verschlüsselt** versendet - in Direkt-Nachrichten mit Post-Quantum-Security.</target>
@@ -1142,6 +1146,10 @@ swipe action</note>
<target>Audio- und Videoanrufe</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio call" xml:space="preserve">
<source>Audio call</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio/video calls" xml:space="preserve">
<source>Audio/video calls</source>
<target>Audio-/Video-Anrufe</target>
@@ -2579,6 +2587,14 @@ swipe action</note>
<target>Nachricht des Mitglieds löschen?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete member messages" xml:space="preserve">
<source>Delete member messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete member messages?" xml:space="preserve">
<source>Delete member messages?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Delete message?" xml:space="preserve">
<source>Delete message?</source>
<target>Die Nachricht löschen?</target>
@@ -2587,7 +2603,8 @@ swipe action</note>
<trans-unit id="Delete messages" xml:space="preserve">
<source>Delete messages</source>
<target>Nachrichten löschen</target>
<note>alert button</note>
<note>alert action
alert button</note>
</trans-unit>
<trans-unit id="Delete messages after" xml:space="preserve">
<source>Delete messages after</source>
@@ -3851,6 +3868,10 @@ snd error text</note>
<target>Dateien und Medien sind nicht erlaubt!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter" xml:space="preserve">
<source>Filter</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter unread and favorite chats." xml:space="preserve">
<source>Filter unread and favorite chats.</source>
<target>Nach ungelesenen und favorisierten Chats filtern.</target>
@@ -4335,6 +4356,10 @@ Fehler: %2$@</target>
<target>Das Bild wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Images" xml:space="preserve">
<source>Images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Immediately" xml:space="preserve">
<source>Immediately</source>
<target>Sofort</target>
@@ -4594,6 +4619,10 @@ Weitere Verbesserungen sind bald verfügbar!</target>
<target>Freunde einladen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invite member" xml:space="preserve">
<source>Invite member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invite members" xml:space="preserve">
<source>Invite members</source>
<target>Mitglieder einladen</target>
@@ -4817,6 +4846,10 @@ Das ist Ihr Link für die Gruppe %@!</target>
<target>Verknüpfte Desktops</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Links" xml:space="preserve">
<source>Links</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="List" xml:space="preserve">
<source>List</source>
<target>Liste</target>
@@ -4942,6 +4975,10 @@ Das ist Ihr Link für die Gruppe %@!</target>
<target>Mitglied ist gelöscht - Anfrage kann nicht angenommen werden</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Member messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>Member messages will be deleted - this cannot be undone!</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member reports" xml:space="preserve">
<source>Member reports</source>
<target>Mitglieder-Meldungen</target>
@@ -4965,12 +5002,12 @@ Das ist Ihr Link für die Gruppe %@!</target>
<trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve">
<source>Member will be removed from chat - this cannot be undone!</source>
<target>Das Mitglied wird aus dem Chat entfernt. Dies kann nicht rückgängig gemacht werden!</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve">
<source>Member will be removed from group - this cannot be undone!</source>
<target>Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden!</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member will join the group, accept member?" xml:space="preserve">
<source>Member will join the group, accept member?</source>
@@ -6595,7 +6632,11 @@ swipe action</note>
<trans-unit id="Remove" xml:space="preserve">
<source>Remove</source>
<target>Entfernen</target>
<note>No comment provided by engineer.</note>
<note>alert action</note>
</trans-unit>
<trans-unit id="Remove and delete messages" xml:space="preserve">
<source>Remove and delete messages</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
@@ -6620,7 +6661,7 @@ swipe action</note>
<trans-unit id="Remove member?" xml:space="preserve">
<source>Remove member?</source>
<target>Das Mitglied entfernen?</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Remove passphrase from keychain?" xml:space="preserve">
<source>Remove passphrase from keychain?</source>
@@ -7038,11 +7079,31 @@ chat item action</note>
<target>In der Suchleiste werden nun auch Einladungslinks angenommen.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search files" xml:space="preserve">
<source>Search files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search images" xml:space="preserve">
<source>Search images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search links" xml:space="preserve">
<source>Search links</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<target>Suchen oder SimpleX-Link einfügen</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search videos" xml:space="preserve">
<source>Search videos</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search voice messages" xml:space="preserve">
<source>Search voice messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secondary" xml:space="preserve">
<source>Secondary</source>
<target>Zweite Farbe</target>
@@ -8918,6 +8979,10 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
<target>Das Video wird heruntergeladen, sobald Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Videos" xml:space="preserve">
<source>Videos</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Videos and files up to 1gb" xml:space="preserve">
<source>Videos and files up to 1gb</source>
<target>Videos und Dateien bis zu 1GB</target>
@@ -792,6 +792,11 @@ swipe action</note>
<target>All group members will remain connected.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages" xml:space="preserve">
<source>All messages</source>
<target>All messages</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve">
<source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source>
<target>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</target>
@@ -1142,6 +1147,11 @@ swipe action</note>
<target>Audio and video calls</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio call" xml:space="preserve">
<source>Audio call</source>
<target>Audio call</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio/video calls" xml:space="preserve">
<source>Audio/video calls</source>
<target>Audio/video calls</target>
@@ -2579,6 +2589,16 @@ swipe action</note>
<target>Delete member message?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete member messages" xml:space="preserve">
<source>Delete member messages</source>
<target>Delete member messages</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete member messages?" xml:space="preserve">
<source>Delete member messages?</source>
<target>Delete member messages?</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Delete message?" xml:space="preserve">
<source>Delete message?</source>
<target>Delete message?</target>
@@ -2587,7 +2607,8 @@ swipe action</note>
<trans-unit id="Delete messages" xml:space="preserve">
<source>Delete messages</source>
<target>Delete messages</target>
<note>alert button</note>
<note>alert action
alert button</note>
</trans-unit>
<trans-unit id="Delete messages after" xml:space="preserve">
<source>Delete messages after</source>
@@ -3851,6 +3872,11 @@ snd error text</note>
<target>Files and media prohibited!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter" xml:space="preserve">
<source>Filter</source>
<target>Filter</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter unread and favorite chats." xml:space="preserve">
<source>Filter unread and favorite chats.</source>
<target>Filter unread and favorite chats.</target>
@@ -4335,6 +4361,11 @@ Error: %2$@</target>
<target>Image will be received when your contact is online, please wait or check later!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Images" xml:space="preserve">
<source>Images</source>
<target>Images</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Immediately" xml:space="preserve">
<source>Immediately</source>
<target>Immediately</target>
@@ -4594,6 +4625,11 @@ More improvements are coming soon!</target>
<target>Invite friends</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invite member" xml:space="preserve">
<source>Invite member</source>
<target>Invite member</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invite members" xml:space="preserve">
<source>Invite members</source>
<target>Invite members</target>
@@ -4817,6 +4853,11 @@ This is your link for group %@!</target>
<target>Linked desktops</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Links" xml:space="preserve">
<source>Links</source>
<target>Links</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="List" xml:space="preserve">
<source>List</source>
<target>List</target>
@@ -4942,6 +4983,11 @@ This is your link for group %@!</target>
<target>Member is deleted - can't accept request</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Member messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>Member messages will be deleted - this cannot be undone!</source>
<target>Member messages will be deleted - this cannot be undone!</target>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member reports" xml:space="preserve">
<source>Member reports</source>
<target>Member reports</target>
@@ -4965,12 +5011,12 @@ This is your link for group %@!</target>
<trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve">
<source>Member will be removed from chat - this cannot be undone!</source>
<target>Member will be removed from chat - this cannot be undone!</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve">
<source>Member will be removed from group - this cannot be undone!</source>
<target>Member will be removed from group - this cannot be undone!</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member will join the group, accept member?" xml:space="preserve">
<source>Member will join the group, accept member?</source>
@@ -6595,7 +6641,12 @@ swipe action</note>
<trans-unit id="Remove" xml:space="preserve">
<source>Remove</source>
<target>Remove</target>
<note>No comment provided by engineer.</note>
<note>alert action</note>
</trans-unit>
<trans-unit id="Remove and delete messages" xml:space="preserve">
<source>Remove and delete messages</source>
<target>Remove and delete messages</target>
<note>alert action</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
@@ -6620,7 +6671,7 @@ swipe action</note>
<trans-unit id="Remove member?" xml:space="preserve">
<source>Remove member?</source>
<target>Remove member?</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Remove passphrase from keychain?" xml:space="preserve">
<source>Remove passphrase from keychain?</source>
@@ -7038,11 +7089,36 @@ chat item action</note>
<target>Search bar accepts invitation links.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search files" xml:space="preserve">
<source>Search files</source>
<target>Search files</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search images" xml:space="preserve">
<source>Search images</source>
<target>Search images</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search links" xml:space="preserve">
<source>Search links</source>
<target>Search links</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<target>Search or paste SimpleX link</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search videos" xml:space="preserve">
<source>Search videos</source>
<target>Search videos</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search voice messages" xml:space="preserve">
<source>Search voice messages</source>
<target>Search voice messages</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secondary" xml:space="preserve">
<source>Secondary</source>
<target>Secondary</target>
@@ -8918,6 +8994,11 @@ To connect, please ask your contact to create another connection link and check
<target>Video will be received when your contact is online, please wait or check later!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Videos" xml:space="preserve">
<source>Videos</source>
<target>Videos</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Videos and files up to 1gb" xml:space="preserve">
<source>Videos and files up to 1gb</source>
<target>Videos and files up to 1gb</target>
@@ -792,6 +792,10 @@ swipe action</note>
<target>Todos los miembros del grupo permanecerán conectados.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages" xml:space="preserve">
<source>All messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve">
<source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source>
<target>Todos los mensajes y archivos son enviados **cifrados de extremo a extremo** y con seguridad de cifrado postcuántico en mensajes directos.</target>
@@ -1142,6 +1146,10 @@ swipe action</note>
<target>Llamadas y videollamadas</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio call" xml:space="preserve">
<source>Audio call</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio/video calls" xml:space="preserve">
<source>Audio/video calls</source>
<target>Llamadas y videollamadas</target>
@@ -2579,6 +2587,14 @@ swipe action</note>
<target>¿Eliminar el mensaje de miembro?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete member messages" xml:space="preserve">
<source>Delete member messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete member messages?" xml:space="preserve">
<source>Delete member messages?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Delete message?" xml:space="preserve">
<source>Delete message?</source>
<target>¿Eliminar mensaje?</target>
@@ -2587,7 +2603,8 @@ swipe action</note>
<trans-unit id="Delete messages" xml:space="preserve">
<source>Delete messages</source>
<target>Activar</target>
<note>alert button</note>
<note>alert action
alert button</note>
</trans-unit>
<trans-unit id="Delete messages after" xml:space="preserve">
<source>Delete messages after</source>
@@ -3851,6 +3868,10 @@ snd error text</note>
<target>¡Archivos y multimedia no permitidos!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter" xml:space="preserve">
<source>Filter</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter unread and favorite chats." xml:space="preserve">
<source>Filter unread and favorite chats.</source>
<target>Filtra chats no leídos y favoritos.</target>
@@ -4335,6 +4356,10 @@ Error: %2$@</target>
<target>La imagen se recibirá cuando el contacto esté en línea, ¡por favor espera o revisa más tarde!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Images" xml:space="preserve">
<source>Images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Immediately" xml:space="preserve">
<source>Immediately</source>
<target>Inmediatamente</target>
@@ -4594,6 +4619,10 @@ More improvements are coming soon!</source>
<target>Invitar amigos</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invite member" xml:space="preserve">
<source>Invite member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invite members" xml:space="preserve">
<source>Invite members</source>
<target>Invitar miembros</target>
@@ -4817,6 +4846,10 @@ This is your link for group %@!</source>
<target>Ordenadores enlazados</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Links" xml:space="preserve">
<source>Links</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="List" xml:space="preserve">
<source>List</source>
<target>Lista</target>
@@ -4942,6 +4975,10 @@ This is your link for group %@!</source>
<target>Miembro eliminado, no puede aceptar solicitudes</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Member messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>Member messages will be deleted - this cannot be undone!</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member reports" xml:space="preserve">
<source>Member reports</source>
<target>Informes de miembros</target>
@@ -4965,12 +5002,12 @@ This is your link for group %@!</source>
<trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve">
<source>Member will be removed from chat - this cannot be undone!</source>
<target>El miembro será eliminado del chat. ¡No puede deshacerse!</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve">
<source>Member will be removed from group - this cannot be undone!</source>
<target>El miembro será expulsado del grupo. ¡No puede deshacerse!</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member will join the group, accept member?" xml:space="preserve">
<source>Member will join the group, accept member?</source>
@@ -5484,7 +5521,7 @@ This is your link for group %@!</source>
</trans-unit>
<trans-unit id="No direct connection yet, message is forwarded by admin." xml:space="preserve">
<source>No direct connection yet, message is forwarded by admin.</source>
<target>Aún no hay conexión directa con este miembro, el mensaje es reenviado por el administrador.</target>
<target>Aún no hay conexión directa, los mensajes son reenviados por el administrador.</target>
<note>item status description</note>
</trans-unit>
<trans-unit id="No filtered chats" xml:space="preserve">
@@ -6595,7 +6632,11 @@ swipe action</note>
<trans-unit id="Remove" xml:space="preserve">
<source>Remove</source>
<target>Eliminar</target>
<note>No comment provided by engineer.</note>
<note>alert action</note>
</trans-unit>
<trans-unit id="Remove and delete messages" xml:space="preserve">
<source>Remove and delete messages</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
@@ -6620,7 +6661,7 @@ swipe action</note>
<trans-unit id="Remove member?" xml:space="preserve">
<source>Remove member?</source>
<target>¿Expulsar miembro?</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Remove passphrase from keychain?" xml:space="preserve">
<source>Remove passphrase from keychain?</source>
@@ -7038,11 +7079,31 @@ chat item action</note>
<target>La barra de búsqueda acepta enlaces de invitación.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search files" xml:space="preserve">
<source>Search files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search images" xml:space="preserve">
<source>Search images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search links" xml:space="preserve">
<source>Search links</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<target>Buscar o pegar enlace SimpleX</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search videos" xml:space="preserve">
<source>Search videos</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search voice messages" xml:space="preserve">
<source>Search voice messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secondary" xml:space="preserve">
<source>Secondary</source>
<target>Secundario</target>
@@ -8918,6 +8979,10 @@ Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión
<target>El vídeo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Videos" xml:space="preserve">
<source>Videos</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Videos and files up to 1gb" xml:space="preserve">
<source>Videos and files up to 1gb</source>
<target>Vídeos y archivos de hasta 1Gb</target>
@@ -9649,7 +9714,7 @@ Repeat connection request?</source>
</trans-unit>
<trans-unit id="accepted you" xml:space="preserve">
<source>accepted you</source>
<target>te ha aceptado</target>
<target>te ha admitido</target>
<note>rcv group event chat item</note>
</trans-unit>
<trans-unit id="admin" xml:space="preserve">
@@ -10623,7 +10688,7 @@ last received msg: %2$@</source>
</trans-unit>
<trans-unit id="you accepted this member" xml:space="preserve">
<source>you accepted this member</source>
<target>has aceptado al miembro</target>
<target>has admitido al miembro</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are observer" xml:space="preserve">
@@ -726,6 +726,10 @@ swipe action</note>
<target>Kaikki ryhmän jäsenet pysyvät yhteydessä.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages" xml:space="preserve">
<source>All messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve">
<source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source>
<note>No comment provided by engineer.</note>
@@ -1042,6 +1046,10 @@ swipe action</note>
<target>Ääni- ja videopuhelut</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio call" xml:space="preserve">
<source>Audio call</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio/video calls" xml:space="preserve">
<source>Audio/video calls</source>
<target>Ääni/videopuhelut</target>
@@ -2328,6 +2336,14 @@ swipe action</note>
<target>Poista jäsenviesti?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete member messages" xml:space="preserve">
<source>Delete member messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete member messages?" xml:space="preserve">
<source>Delete member messages?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Delete message?" xml:space="preserve">
<source>Delete message?</source>
<target>Poista viesti?</target>
@@ -2336,7 +2352,8 @@ swipe action</note>
<trans-unit id="Delete messages" xml:space="preserve">
<source>Delete messages</source>
<target>Poista viestit</target>
<note>alert button</note>
<note>alert action
alert button</note>
</trans-unit>
<trans-unit id="Delete messages after" xml:space="preserve">
<source>Delete messages after</source>
@@ -3483,6 +3500,10 @@ snd error text</note>
<target>Tiedostot ja media kielletty!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter" xml:space="preserve">
<source>Filter</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter unread and favorite chats." xml:space="preserve">
<source>Filter unread and favorite chats.</source>
<target>Suodata lukemattomia- ja suosikkikeskusteluja.</target>
@@ -3924,6 +3945,10 @@ Error: %2$@</source>
<target>Kuva vastaanotetaan, kun kontaktisi on verkossa, odota tai tarkista myöhemmin!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Images" xml:space="preserve">
<source>Images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Immediately" xml:space="preserve">
<source>Immediately</source>
<target>Heti</target>
@@ -4159,6 +4184,10 @@ More improvements are coming soon!</source>
<target>Kutsu ystäviä</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invite member" xml:space="preserve">
<source>Invite member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invite members" xml:space="preserve">
<source>Invite members</source>
<target>Kutsu jäseniä</target>
@@ -4366,6 +4395,10 @@ This is your link for group %@!</source>
<source>Linked desktops</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Links" xml:space="preserve">
<source>Links</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="List" xml:space="preserve">
<source>List</source>
<note>swipe action</note>
@@ -4481,6 +4514,10 @@ This is your link for group %@!</source>
<source>Member is deleted - can't accept request</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Member messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>Member messages will be deleted - this cannot be undone!</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member reports" xml:space="preserve">
<source>Member reports</source>
<note>chat feature</note>
@@ -4501,12 +4538,12 @@ This is your link for group %@!</source>
</trans-unit>
<trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve">
<source>Member will be removed from chat - this cannot be undone!</source>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve">
<source>Member will be removed from group - this cannot be undone!</source>
<target>Jäsen poistetaan ryhmästä - tätä ei voi perua!</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member will join the group, accept member?" xml:space="preserve">
<source>Member will join the group, accept member?</source>
@@ -5959,7 +5996,11 @@ swipe action</note>
<trans-unit id="Remove" xml:space="preserve">
<source>Remove</source>
<target>Poista</target>
<note>No comment provided by engineer.</note>
<note>alert action</note>
</trans-unit>
<trans-unit id="Remove and delete messages" xml:space="preserve">
<source>Remove and delete messages</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
@@ -5981,7 +6022,7 @@ swipe action</note>
<trans-unit id="Remove member?" xml:space="preserve">
<source>Remove member?</source>
<target>Poista jäsen?</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Remove passphrase from keychain?" xml:space="preserve">
<source>Remove passphrase from keychain?</source>
@@ -6356,10 +6397,30 @@ chat item action</note>
<source>Search bar accepts invitation links.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search files" xml:space="preserve">
<source>Search files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search images" xml:space="preserve">
<source>Search images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search links" xml:space="preserve">
<source>Search links</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search videos" xml:space="preserve">
<source>Search videos</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search voice messages" xml:space="preserve">
<source>Search voice messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secondary" xml:space="preserve">
<source>Secondary</source>
<note>No comment provided by engineer.</note>
@@ -8038,6 +8099,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja
<target>Video vastaanotetaan, kun kontaktisi on online-tilassa, odota tai tarkista myöhemmin!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Videos" xml:space="preserve">
<source>Videos</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Videos and files up to 1gb" xml:space="preserve">
<source>Videos and files up to 1gb</source>
<target>Videot ja tiedostot 1 Gt asti</target>
@@ -792,6 +792,10 @@ swipe action</note>
<target>Tous les membres du groupe resteront connectés.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages" xml:space="preserve">
<source>All messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve">
<source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source>
<target>Tous les messages et fichiers sont envoyés **chiffrés de bout en bout**, avec une sécurité post-quantique dans les messages directs.</target>
@@ -1141,6 +1145,10 @@ swipe action</note>
<target>Appels audio et vidéo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio call" xml:space="preserve">
<source>Audio call</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio/video calls" xml:space="preserve">
<source>Audio/video calls</source>
<target>Appels audio/vidéo</target>
@@ -2564,6 +2572,14 @@ swipe action</note>
<target>Supprimer le message de ce membre ?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete member messages" xml:space="preserve">
<source>Delete member messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete member messages?" xml:space="preserve">
<source>Delete member messages?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Delete message?" xml:space="preserve">
<source>Delete message?</source>
<target>Supprimer le message?</target>
@@ -2572,7 +2588,8 @@ swipe action</note>
<trans-unit id="Delete messages" xml:space="preserve">
<source>Delete messages</source>
<target>Supprimer les messages</target>
<note>alert button</note>
<note>alert action
alert button</note>
</trans-unit>
<trans-unit id="Delete messages after" xml:space="preserve">
<source>Delete messages after</source>
@@ -3822,6 +3839,10 @@ snd error text</note>
<target>Fichiers et médias interdits !</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter" xml:space="preserve">
<source>Filter</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter unread and favorite chats." xml:space="preserve">
<source>Filter unread and favorite chats.</source>
<target>Filtrer les messages non lus et favoris.</target>
@@ -4296,6 +4317,10 @@ Erreur: %2$@</target>
<target>L'image sera reçue quand votre contact sera en ligne, merci d'attendre ou de revenir plus tard!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Images" xml:space="preserve">
<source>Images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Immediately" xml:space="preserve">
<source>Immediately</source>
<target>Immédiatement</target>
@@ -4548,6 +4573,10 @@ D'autres améliorations sont à venir!</target>
<target>Inviter des amis</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invite member" xml:space="preserve">
<source>Invite member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invite members" xml:space="preserve">
<source>Invite members</source>
<target>Inviter des membres</target>
@@ -4769,6 +4798,10 @@ Voici votre lien pour le groupe %@ !</target>
<target>Bureaux liés</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Links" xml:space="preserve">
<source>Links</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="List" xml:space="preserve">
<source>List</source>
<note>swipe action</note>
@@ -4887,6 +4920,10 @@ Voici votre lien pour le groupe %@ !</target>
<source>Member is deleted - can't accept request</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Member messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>Member messages will be deleted - this cannot be undone!</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member reports" xml:space="preserve">
<source>Member reports</source>
<note>chat feature</note>
@@ -4909,12 +4946,12 @@ Voici votre lien pour le groupe %@ !</target>
<trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve">
<source>Member will be removed from chat - this cannot be undone!</source>
<target>Le membre sera retiré de la discussion - cela ne peut pas être annulé !</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve">
<source>Member will be removed from group - this cannot be undone!</source>
<target>Ce membre sera retiré du groupe - impossible de revenir en arrière!</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member will join the group, accept member?" xml:space="preserve">
<source>Member will join the group, accept member?</source>
@@ -6489,7 +6526,11 @@ swipe action</note>
<trans-unit id="Remove" xml:space="preserve">
<source>Remove</source>
<target>Supprimer</target>
<note>No comment provided by engineer.</note>
<note>alert action</note>
</trans-unit>
<trans-unit id="Remove and delete messages" xml:space="preserve">
<source>Remove and delete messages</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
@@ -6513,7 +6554,7 @@ swipe action</note>
<trans-unit id="Remove member?" xml:space="preserve">
<source>Remove member?</source>
<target>Retirer ce membre?</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Remove passphrase from keychain?" xml:space="preserve">
<source>Remove passphrase from keychain?</source>
@@ -6912,11 +6953,31 @@ chat item action</note>
<target>La barre de recherche accepte les liens d'invitation.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search files" xml:space="preserve">
<source>Search files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search images" xml:space="preserve">
<source>Search images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search links" xml:space="preserve">
<source>Search links</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<target>Rechercher ou coller un lien SimpleX</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search videos" xml:space="preserve">
<source>Search videos</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search voice messages" xml:space="preserve">
<source>Search voice messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secondary" xml:space="preserve">
<source>Secondary</source>
<target>Secondaire</target>
@@ -8743,6 +8804,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien
<target>La vidéo ne sera reçue que lorsque votre contact sera en ligne. Veuillez patienter ou vérifier plus tard !</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Videos" xml:space="preserve">
<source>Videos</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Videos and files up to 1gb" xml:space="preserve">
<source>Videos and files up to 1gb</source>
<target>Vidéos et fichiers jusqu'à 1Go</target>
File diff suppressed because it is too large Load Diff
@@ -792,6 +792,10 @@ swipe action</note>
<target>Tutti i membri del gruppo resteranno connessi.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages" xml:space="preserve">
<source>All messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages." xml:space="preserve">
<source>All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.</source>
<target>Tutti i messaggi e i file vengono inviati **crittografati end-to-end**, con sicurezza resistenti alla quantistica nei messaggi diretti.</target>
@@ -1142,6 +1146,10 @@ swipe action</note>
<target>Chiamate audio e video</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio call" xml:space="preserve">
<source>Audio call</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Audio/video calls" xml:space="preserve">
<source>Audio/video calls</source>
<target>Chiamate audio/video</target>
@@ -2579,6 +2587,14 @@ swipe action</note>
<target>Eliminare il messaggio del membro?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete member messages" xml:space="preserve">
<source>Delete member messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Delete member messages?" xml:space="preserve">
<source>Delete member messages?</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Delete message?" xml:space="preserve">
<source>Delete message?</source>
<target>Eliminare il messaggio?</target>
@@ -2587,7 +2603,8 @@ swipe action</note>
<trans-unit id="Delete messages" xml:space="preserve">
<source>Delete messages</source>
<target>Elimina messaggi</target>
<note>alert button</note>
<note>alert action
alert button</note>
</trans-unit>
<trans-unit id="Delete messages after" xml:space="preserve">
<source>Delete messages after</source>
@@ -3851,6 +3868,10 @@ snd error text</note>
<target>File e contenuti multimediali vietati!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter" xml:space="preserve">
<source>Filter</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Filter unread and favorite chats." xml:space="preserve">
<source>Filter unread and favorite chats.</source>
<target>Filtra le chat non lette e preferite.</target>
@@ -4335,6 +4356,10 @@ Errore: %2$@</target>
<target>L'immagine verrà ricevuta quando il tuo contatto sarà in linea, aspetta o controlla più tardi!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Images" xml:space="preserve">
<source>Images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Immediately" xml:space="preserve">
<source>Immediately</source>
<target>Immediatamente</target>
@@ -4594,6 +4619,10 @@ Altri miglioramenti sono in arrivo!</target>
<target>Invita amici</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invite member" xml:space="preserve">
<source>Invite member</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Invite members" xml:space="preserve">
<source>Invite members</source>
<target>Invita membri</target>
@@ -4817,6 +4846,10 @@ Questo è il tuo link per il gruppo %@!</target>
<target>Desktop collegati</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Links" xml:space="preserve">
<source>Links</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="List" xml:space="preserve">
<source>List</source>
<target>Elenco</target>
@@ -4942,6 +4975,10 @@ Questo è il tuo link per il gruppo %@!</target>
<target>Il membro è eliminato - impossibile accettare la richiesta</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Member messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>Member messages will be deleted - this cannot be undone!</source>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member reports" xml:space="preserve">
<source>Member reports</source>
<target>Segnalazioni dei membri</target>
@@ -4965,12 +5002,12 @@ Questo è il tuo link per il gruppo %@!</target>
<trans-unit id="Member will be removed from chat - this cannot be undone!" xml:space="preserve">
<source>Member will be removed from chat - this cannot be undone!</source>
<target>Il membro verrà rimosso dalla chat, non è reversibile!</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve">
<source>Member will be removed from group - this cannot be undone!</source>
<target>Il membro verrà rimosso dal gruppo, non è reversibile!</target>
<note>No comment provided by engineer.</note>
<note>alert message</note>
</trans-unit>
<trans-unit id="Member will join the group, accept member?" xml:space="preserve">
<source>Member will join the group, accept member?</source>
@@ -5854,7 +5891,7 @@ Richiede l'attivazione della VPN.</target>
</trans-unit>
<trans-unit id="Open new group" xml:space="preserve">
<source>Open new group</source>
<target>Apri un gruppo nuovo</target>
<target>Apri il nuovo gruppo</target>
<note>new chat action</note>
</trans-unit>
<trans-unit id="Open to accept" xml:space="preserve">
@@ -6595,7 +6632,11 @@ swipe action</note>
<trans-unit id="Remove" xml:space="preserve">
<source>Remove</source>
<target>Rimuovi</target>
<note>No comment provided by engineer.</note>
<note>alert action</note>
</trans-unit>
<trans-unit id="Remove and delete messages" xml:space="preserve">
<source>Remove and delete messages</source>
<note>alert action</note>
</trans-unit>
<trans-unit id="Remove archive?" xml:space="preserve">
<source>Remove archive?</source>
@@ -6620,7 +6661,7 @@ swipe action</note>
<trans-unit id="Remove member?" xml:space="preserve">
<source>Remove member?</source>
<target>Rimuovere il membro?</target>
<note>No comment provided by engineer.</note>
<note>alert title</note>
</trans-unit>
<trans-unit id="Remove passphrase from keychain?" xml:space="preserve">
<source>Remove passphrase from keychain?</source>
@@ -7038,11 +7079,31 @@ chat item action</note>
<target>La barra di ricerca accetta i link di invito.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search files" xml:space="preserve">
<source>Search files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search images" xml:space="preserve">
<source>Search images</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search links" xml:space="preserve">
<source>Search links</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source>
<target>Cerca o incolla un link SimpleX</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search videos" xml:space="preserve">
<source>Search videos</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Search voice messages" xml:space="preserve">
<source>Search voice messages</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Secondary" xml:space="preserve">
<source>Secondary</source>
<target>Secondario</target>
@@ -8918,6 +8979,10 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e
<target>Il video verrà ricevuto quando il tuo contatto sarà in linea, attendi o controlla più tardi!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Videos" xml:space="preserve">
<source>Videos</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Videos and files up to 1gb" xml:space="preserve">
<source>Videos and files up to 1gb</source>
<target>Video e file fino a 1 GB</target>

Some files were not shown because too many files have changed in this diff Show More